feat(im): 添加即时通讯服务端SDK
- 实现用户登录认证功能,支持签名验证 - 添加消息发送、撤回及历史记录查询功能 - 实现会话管理功能,包括置顶、静音、已读标记等 - 添加好友系统功能,支持好友申请、同意、删除操作 - 实现群组管理功能,包括创建、解散、成员管理等 - 添加黑名单管理功能 - 实现推送服务集成,支持消息推送注册和发送 - 添加应用版本更新功能,支持Android应用和RN Bundle更新 - 实现关键词过滤和全局禁言管理功能 - 添加管理员消息查询和管理功能 - 集成Webhook配置管理功能 - 实现多服务器URL配置支持,包括基础服务、推送服务和更新服务 - 添加multipart表单数据上传支持,用于文件传输 - 实现统一的API请求处理和响应解析机制
这个提交包含在:
父节点
bbf1fd0769
当前提交
c217e96482
@ -14,7 +14,10 @@ import java.net.URLEncoder;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
@ -31,6 +34,8 @@ public final class XuqmImServerSdk {
|
|||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
|
private final String pushBaseUrl;
|
||||||
|
private final String updateBaseUrl;
|
||||||
private final String appId;
|
private final String appId;
|
||||||
private final String appSecret;
|
private final String appSecret;
|
||||||
private final Supplier<String> bearerTokenSupplier;
|
private final Supplier<String> bearerTokenSupplier;
|
||||||
@ -41,6 +46,8 @@ public final class XuqmImServerSdk {
|
|||||||
.registerModule(new JavaTimeModule())
|
.registerModule(new JavaTimeModule())
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
this.baseUrl = trimTrailingSlash(Objects.requireNonNull(builder.baseUrl, "baseUrl"));
|
this.baseUrl = trimTrailingSlash(Objects.requireNonNull(builder.baseUrl, "baseUrl"));
|
||||||
|
this.pushBaseUrl = trimTrailingSlash(builder.pushBaseUrl == null ? builder.baseUrl : builder.pushBaseUrl);
|
||||||
|
this.updateBaseUrl = trimTrailingSlash(builder.updateBaseUrl == null ? builder.baseUrl : builder.updateBaseUrl);
|
||||||
this.appId = Objects.requireNonNull(builder.appId, "appId");
|
this.appId = Objects.requireNonNull(builder.appId, "appId");
|
||||||
this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret");
|
this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret");
|
||||||
this.bearerTokenSupplier = builder.bearerTokenSupplier;
|
this.bearerTokenSupplier = builder.bearerTokenSupplier;
|
||||||
@ -405,6 +412,216 @@ public final class XuqmImServerSdk {
|
|||||||
return response.data();
|
return response.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void registerPushToken(String userId, String vendor, String token) {
|
||||||
|
request(
|
||||||
|
"POST",
|
||||||
|
buildUri(pushBaseUrl, "/api/push/register", Map.of(
|
||||||
|
"appId", appId,
|
||||||
|
"userId", userId,
|
||||||
|
"vendor", vendor,
|
||||||
|
"token", token
|
||||||
|
)),
|
||||||
|
null,
|
||||||
|
publicHeaders(),
|
||||||
|
new TypeReference<ApiResponse<Void>>() {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPush(String userId, String title, String body, String payload) {
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("appId", appId);
|
||||||
|
query.put("userId", userId);
|
||||||
|
query.put("title", title);
|
||||||
|
query.put("body", body);
|
||||||
|
if (payload != null) {
|
||||||
|
query.put("payload", payload);
|
||||||
|
}
|
||||||
|
request(
|
||||||
|
"POST",
|
||||||
|
buildUri(pushBaseUrl, "/api/push/send", query),
|
||||||
|
null,
|
||||||
|
publicHeaders(),
|
||||||
|
new TypeReference<ApiResponse<Void>>() {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> checkAppUpdate(String platform, int currentVersionCode) {
|
||||||
|
ApiResponse<Map<String, Object>> response = request(
|
||||||
|
"GET",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/updates/app/check", Map.of(
|
||||||
|
"appId", appId,
|
||||||
|
"platform", platform,
|
||||||
|
"currentVersionCode", String.valueOf(currentVersionCode)
|
||||||
|
)),
|
||||||
|
null,
|
||||||
|
publicHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppVersionView uploadAppVersion(
|
||||||
|
String platform,
|
||||||
|
String versionName,
|
||||||
|
int versionCode,
|
||||||
|
String changeLog,
|
||||||
|
boolean forceUpdate,
|
||||||
|
Path apkFile) {
|
||||||
|
Map<String, String> form = new LinkedHashMap<>();
|
||||||
|
form.put("appId", appId);
|
||||||
|
form.put("platform", platform);
|
||||||
|
form.put("versionName", versionName);
|
||||||
|
form.put("versionCode", String.valueOf(versionCode));
|
||||||
|
if (changeLog != null) {
|
||||||
|
form.put("changeLog", changeLog);
|
||||||
|
}
|
||||||
|
form.put("forceUpdate", String.valueOf(forceUpdate));
|
||||||
|
ApiResponse<AppVersionView> response = multipartRequest(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/updates/app/upload", Map.of()),
|
||||||
|
form,
|
||||||
|
"apkFile",
|
||||||
|
apkFile,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppVersionView publishAppVersion(String id) {
|
||||||
|
ApiResponse<AppVersionView> response = request(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/publish", Map.of()),
|
||||||
|
null,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppVersionView unpublishAppVersion(String id) {
|
||||||
|
ApiResponse<AppVersionView> response = request(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/unpublish", Map.of()),
|
||||||
|
null,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppVersionView grayAppVersion(String id, boolean enabled, int percent) {
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("enabled", enabled);
|
||||||
|
body.put("percent", percent);
|
||||||
|
ApiResponse<AppVersionView> response = request(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/gray", Map.of()),
|
||||||
|
body,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AppVersionView> listAppVersions(String platform) {
|
||||||
|
ApiResponse<List<AppVersionView>> response = request(
|
||||||
|
"GET",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/updates/app/list", Map.of("appId", appId, "platform", platform)),
|
||||||
|
null,
|
||||||
|
publicHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RnBundleView checkRnUpdate(String moduleId, String platform, String currentVersion) {
|
||||||
|
ApiResponse<RnBundleView> response = request(
|
||||||
|
"GET",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/rn/update/check", Map.of(
|
||||||
|
"appId", appId,
|
||||||
|
"moduleId", moduleId,
|
||||||
|
"platform", platform,
|
||||||
|
"currentVersion", currentVersion
|
||||||
|
)),
|
||||||
|
null,
|
||||||
|
publicHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RnBundleView uploadRnBundle(
|
||||||
|
String moduleId,
|
||||||
|
String platform,
|
||||||
|
String version,
|
||||||
|
String minCommonVersion,
|
||||||
|
String note,
|
||||||
|
Path bundle) {
|
||||||
|
Map<String, String> form = new LinkedHashMap<>();
|
||||||
|
form.put("appId", appId);
|
||||||
|
form.put("moduleId", moduleId);
|
||||||
|
form.put("platform", platform);
|
||||||
|
form.put("version", version);
|
||||||
|
if (minCommonVersion != null) {
|
||||||
|
form.put("minCommonVersion", minCommonVersion);
|
||||||
|
}
|
||||||
|
if (note != null) {
|
||||||
|
form.put("note", note);
|
||||||
|
}
|
||||||
|
ApiResponse<RnBundleView> response = multipartRequest(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/rn/upload", Map.of()),
|
||||||
|
form,
|
||||||
|
"bundle",
|
||||||
|
bundle,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RnBundleView publishRnBundle(String id) {
|
||||||
|
ApiResponse<RnBundleView> response = request(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/publish", Map.of()),
|
||||||
|
null,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RnBundleView unpublishRnBundle(String id) {
|
||||||
|
ApiResponse<RnBundleView> response = request(
|
||||||
|
"POST",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/unpublish", Map.of()),
|
||||||
|
null,
|
||||||
|
authorizedHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RnBundleView> listRnBundles(String moduleId, String platform) {
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("appId", appId);
|
||||||
|
if (moduleId != null) {
|
||||||
|
query.put("moduleId", moduleId);
|
||||||
|
}
|
||||||
|
if (platform != null) {
|
||||||
|
query.put("platform", platform);
|
||||||
|
}
|
||||||
|
ApiResponse<List<RnBundleView>> response = request(
|
||||||
|
"GET",
|
||||||
|
buildUri(updateBaseUrl, "/api/v1/rn/list", query),
|
||||||
|
null,
|
||||||
|
publicHeaders(),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
return response.data();
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> listFriends() {
|
public List<String> listFriends() {
|
||||||
ApiResponse<List<String>> response = request(
|
ApiResponse<List<String>> response = request(
|
||||||
"GET",
|
"GET",
|
||||||
@ -821,6 +1038,10 @@ public final class XuqmImServerSdk {
|
|||||||
return Map.of("Authorization", "Bearer " + token);
|
return Map.of("Authorization", "Bearer " + token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, String> publicHeaders() {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
private <T> ApiResponse<T> request(
|
private <T> ApiResponse<T> request(
|
||||||
String method,
|
String method,
|
||||||
URI uri,
|
URI uri,
|
||||||
@ -856,6 +1077,66 @@ public final class XuqmImServerSdk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <T> ApiResponse<T> multipartRequest(
|
||||||
|
String method,
|
||||||
|
URI uri,
|
||||||
|
Map<String, String> formFields,
|
||||||
|
String fileFieldName,
|
||||||
|
Path file,
|
||||||
|
Map<String, String> headers,
|
||||||
|
TypeReference<ApiResponse<T>> responseType
|
||||||
|
) {
|
||||||
|
String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", "");
|
||||||
|
try {
|
||||||
|
HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, fileFieldName, file);
|
||||||
|
Map<String, String> mergedHeaders = new LinkedHashMap<>(headers);
|
||||||
|
mergedHeaders.put("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||||
|
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
||||||
|
.headers(flatten(mergedHeaders))
|
||||||
|
.method(method, bodyPublisher);
|
||||||
|
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() >= 400) {
|
||||||
|
throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body());
|
||||||
|
}
|
||||||
|
ApiResponse<T> apiResponse = objectMapper.readValue(response.body(), responseType);
|
||||||
|
if (apiResponse.code() != 200) {
|
||||||
|
throw new ImSdkException(apiResponse.message());
|
||||||
|
}
|
||||||
|
return apiResponse;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImSdkException("Request failed: " + e.getMessage(), e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new ImSdkException("Request interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequest.BodyPublisher ofMultipartData(
|
||||||
|
String boundary,
|
||||||
|
Map<String, String> formFields,
|
||||||
|
String fileFieldName,
|
||||||
|
Path file
|
||||||
|
) throws IOException {
|
||||||
|
var byteArrays = new ArrayList<byte[]>();
|
||||||
|
Charset charset = StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : formFields.entrySet()) {
|
||||||
|
byteArrays.add(("--" + boundary + "\r\n").getBytes(charset));
|
||||||
|
byteArrays.add(("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n").getBytes(charset));
|
||||||
|
byteArrays.add(entry.getValue().getBytes(charset));
|
||||||
|
byteArrays.add("\r\n".getBytes(charset));
|
||||||
|
}
|
||||||
|
|
||||||
|
byteArrays.add(("--" + boundary + "\r\n").getBytes(charset));
|
||||||
|
byteArrays.add(("Content-Disposition: form-data; name=\"" + fileFieldName + "\"; filename=\"" + file.getFileName() + "\"\r\n").getBytes(charset));
|
||||||
|
byteArrays.add(("Content-Type: application/octet-stream\r\n\r\n").getBytes(charset));
|
||||||
|
byteArrays.add(Files.readAllBytes(file));
|
||||||
|
byteArrays.add("\r\n".getBytes(charset));
|
||||||
|
byteArrays.add(("--" + boundary + "--\r\n").getBytes(charset));
|
||||||
|
|
||||||
|
return HttpRequest.BodyPublishers.ofByteArrays(byteArrays);
|
||||||
|
}
|
||||||
|
|
||||||
private URI buildUri(String path, Map<String, String> query) {
|
private URI buildUri(String path, Map<String, String> query) {
|
||||||
StringBuilder builder = new StringBuilder(baseUrl).append(path);
|
StringBuilder builder = new StringBuilder(baseUrl).append(path);
|
||||||
if (query != null && !query.isEmpty()) {
|
if (query != null && !query.isEmpty()) {
|
||||||
@ -876,6 +1157,26 @@ public final class XuqmImServerSdk {
|
|||||||
return URI.create(builder.toString());
|
return URI.create(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private URI buildUri(String base, String path, Map<String, String> query) {
|
||||||
|
StringBuilder builder = new StringBuilder(trimTrailingSlash(base)).append(path);
|
||||||
|
if (query != null && !query.isEmpty()) {
|
||||||
|
builder.append('?');
|
||||||
|
boolean first = true;
|
||||||
|
for (Map.Entry<String, String> entry : query.entrySet()) {
|
||||||
|
String value = entry.getValue();
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!first) {
|
||||||
|
builder.append('&');
|
||||||
|
}
|
||||||
|
builder.append(encode(entry.getKey())).append('=').append(encode(value));
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return URI.create(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, String> loginQuery(String userId, String nickname, String avatar, long timestamp, String nonce) {
|
private Map<String, String> loginQuery(String userId, String nickname, String avatar, long timestamp, String nonce) {
|
||||||
Map<String, String> query = new LinkedHashMap<>();
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
query.put("appId", appId);
|
query.put("appId", appId);
|
||||||
@ -951,6 +1252,8 @@ public final class XuqmImServerSdk {
|
|||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
private String pushBaseUrl;
|
||||||
|
private String updateBaseUrl;
|
||||||
private String appId;
|
private String appId;
|
||||||
private String appSecret;
|
private String appSecret;
|
||||||
private Supplier<String> bearerTokenSupplier;
|
private Supplier<String> bearerTokenSupplier;
|
||||||
@ -962,6 +1265,16 @@ public final class XuqmImServerSdk {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder pushBaseUrl(String pushBaseUrl) {
|
||||||
|
this.pushBaseUrl = pushBaseUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder updateBaseUrl(String updateBaseUrl) {
|
||||||
|
this.updateBaseUrl = updateBaseUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder appId(String appId) {
|
public Builder appId(String appId) {
|
||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
return this;
|
return this;
|
||||||
@ -1098,6 +1411,39 @@ public final class XuqmImServerSdk {
|
|||||||
Long updatedAt
|
Long updatedAt
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public record AppVersionView(
|
||||||
|
String id,
|
||||||
|
String appId,
|
||||||
|
String platform,
|
||||||
|
String versionName,
|
||||||
|
int versionCode,
|
||||||
|
String downloadUrl,
|
||||||
|
String changeLog,
|
||||||
|
boolean forceUpdate,
|
||||||
|
String publishStatus,
|
||||||
|
String appStoreUrl,
|
||||||
|
String marketUrl,
|
||||||
|
boolean grayEnabled,
|
||||||
|
int grayPercent,
|
||||||
|
Long createdAt
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record RnBundleView(
|
||||||
|
String id,
|
||||||
|
String appId,
|
||||||
|
String moduleId,
|
||||||
|
String platform,
|
||||||
|
String version,
|
||||||
|
String bundleUrl,
|
||||||
|
String md5,
|
||||||
|
String minCommonVersion,
|
||||||
|
String note,
|
||||||
|
String publishStatus,
|
||||||
|
boolean grayEnabled,
|
||||||
|
int grayPercent,
|
||||||
|
Long createdAt
|
||||||
|
) {}
|
||||||
|
|
||||||
public record HistoryQuery(
|
public record HistoryQuery(
|
||||||
String msgType,
|
String msgType,
|
||||||
String keyword,
|
String keyword,
|
||||||
@ -1159,6 +1505,24 @@ public final class XuqmImServerSdk {
|
|||||||
Boolean enabled
|
Boolean enabled
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public record AppVersionUploadRequest(
|
||||||
|
String appId,
|
||||||
|
String platform,
|
||||||
|
String versionName,
|
||||||
|
int versionCode,
|
||||||
|
String changeLog,
|
||||||
|
boolean forceUpdate
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record RnBundleUploadRequest(
|
||||||
|
String appId,
|
||||||
|
String moduleId,
|
||||||
|
String platform,
|
||||||
|
String version,
|
||||||
|
String minCommonVersion,
|
||||||
|
String note
|
||||||
|
) {}
|
||||||
|
|
||||||
public record CreateGroupRequest(
|
public record CreateGroupRequest(
|
||||||
String name,
|
String name,
|
||||||
List<String> memberIds,
|
List<String> memberIds,
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户