XuqmGroup-Server/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java

1670 行
57 KiB
Java

package com.xuqm.im.sdk;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.xuqm.common.model.ApiResponse;
import com.xuqm.common.security.AppRequestSignatureUtil;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Supplier;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class XuqmImServerSdk {
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final String baseUrl;
private final String pushBaseUrl;
private final String updateBaseUrl;
private final String appId;
private final String appSecret;
private final Supplier<String> bearerTokenSupplier;
private XuqmImServerSdk(Builder builder) {
this.httpClient = HttpClient.newHttpClient();
this.objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
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.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret");
this.bearerTokenSupplier = builder.bearerTokenSupplier;
}
public static Builder builder() {
return new Builder();
}
public LoginResponse login(String userId, String nickname, String avatar) {
long timestamp = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString().replace("-", "");
String payload = AppRequestSignatureUtil.payload(appId, userId, nickname, avatar, timestamp, nonce);
String signature = AppRequestSignatureUtil.sign(appSecret, payload);
URI uri = buildUri(
"/api/im/auth/login",
loginQuery(userId, nickname, avatar, timestamp, nonce)
);
ApiResponse<LoginResponse> response = request(
"POST",
uri,
null,
Map.of(
"X-App-Id", appId,
"X-App-Timestamp", String.valueOf(timestamp),
"X-App-Nonce", nonce,
"X-App-Signature", signature
),
new TypeReference<>() {}
);
return response.data();
}
public ImMessage sendMessage(SendMessageRequest request) {
ApiResponse<ImMessage> response = request(
"POST",
buildUri("/api/im/messages/send", Map.of("appId", appId)),
request,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public ImMessage revokeMessage(String messageId) {
ApiResponse<ImMessage> response = request(
"POST",
buildUri("/api/im/messages/" + encode(messageId) + "/revoke", Map.of("appId", appId)),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public ImMessage editMessage(String messageId, String content) {
ApiResponse<ImMessage> response = request(
"PUT",
buildUri("/api/im/messages/" + encode(messageId), Map.of("appId", appId)),
Map.of("content", content),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<ConversationView> listConversations(int size) {
ApiResponse<List<ConversationView>> response = request(
"GET",
buildUri("/api/im/conversations", Map.of("appId", appId, "page", "0", "size", String.valueOf(size))),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public PageResult<ImMessage> fetchHistory(String toId, HistoryQuery query) {
HistoryQuery effective = query == null ? new HistoryQuery() : query;
ApiResponse<PageResult<ImMessage>> response = request(
"GET",
buildUri(
"/api/im/messages/history/" + encode(toId),
queryParams(
effective.msgType(),
effective.keyword(),
effective.startTime(),
effective.endTime(),
effective.page() == null ? 0 : effective.page(),
effective.size() == null ? 20 : effective.size()
)
),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public PageResult<ImMessage> fetchGroupHistory(String groupId, HistoryQuery query) {
HistoryQuery effective = query == null ? new HistoryQuery() : query;
ApiResponse<PageResult<ImMessage>> response = request(
"GET",
buildUri(
"/api/im/messages/group-history/" + encode(groupId),
queryParams(
effective.msgType(),
effective.keyword(),
effective.startTime(),
effective.endTime(),
effective.page() == null ? 0 : effective.page(),
effective.size() == null ? 20 : effective.size()
)
),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<ConversationView> listConversations() {
return listConversations(20);
}
public AccountView getProfile(String userId) {
ApiResponse<AccountView> response = request(
"GET",
buildUri("/api/im/accounts/" + encode(userId), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public AccountView updateProfile(String userId, String nickname, String avatar, String gender) {
Map<String, String> query = appQuery();
if (nickname != null) {
query.put("nickname", nickname);
}
if (avatar != null) {
query.put("avatar", avatar);
}
if (gender != null) {
query.put("gender", gender);
}
ApiResponse<AccountView> response = request(
"PUT",
buildUri("/api/im/accounts/" + encode(userId), query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public PageResult<AccountView> listUsers(int page, int size) {
ApiResponse<PageResult<AccountView>> response = request(
"GET",
buildUri("/api/im/admin/users", queryWithPage(page, size)),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public AccountView registerUser(String userId, String nickname, String avatar) {
Map<String, String> body = new LinkedHashMap<>();
body.put("userId", userId);
if (nickname != null) {
body.put("nickname", nickname);
}
if (avatar != null) {
body.put("avatar", avatar);
}
ApiResponse<AccountView> response = request(
"POST",
buildUri("/api/im/admin/users", appQuery()),
body,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public AccountView updateUserStatus(String userId, String status) {
ApiResponse<AccountView> response = request(
"PUT",
buildUri("/api/im/admin/users/" + encode(userId) + "/status", appQuery()),
Map.of("status", status),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<AccountView> searchUsers(String keyword, int size) {
ApiResponse<List<AccountView>> response = request(
"GET",
buildUri("/api/im/admin/users/search", queryWithSize("keyword", keyword, size)),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public StatsView stats() {
ApiResponse<StatsView> response = request(
"GET",
buildUri("/api/im/admin/stats", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<GroupView> searchGroups(String keyword, int size) {
ApiResponse<List<GroupView>> response = request(
"GET",
buildUri("/api/im/admin/groups/search", queryWithSize("keyword", keyword, size)),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public boolean verifyCallbackSignature(String timestamp, String nonce, String body, String signature) {
long ts;
try {
ts = Long.parseLong(timestamp);
} catch (NumberFormatException e) {
return false;
}
long now = System.currentTimeMillis();
if (Math.abs(now - ts) > 5 * 60 * 1000L) {
return false;
}
String payload = appId + "\n" + timestamp + "\n" + normalize(nonce) + "\n" + sha256Hex(body);
String expected = hmacSha256Hex(appSecret, payload);
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
normalize(signature).getBytes(StandardCharsets.UTF_8)
);
}
public WebhookCallbackEnvelope parseCallbackEnvelope(String body) {
try {
JsonNode root = objectMapper.readTree(body);
return new WebhookCallbackEnvelope(
text(root, "callbackId"),
text(root, "callbackType"),
text(root, "callbackEvent"),
longValue(root, "requestTime"),
root.get("payload"),
text(root, "signature"),
text(root, "appId")
);
} catch (JsonProcessingException e) {
throw new ImSdkException("Invalid callback body", e);
}
}
public PageResult<ImMessage> searchMessages(
String keyword,
String chatType,
String msgType,
LocalDateTime startTime,
LocalDateTime endTime,
int page,
int size) {
Map<String, String> query = appQuery();
if (keyword != null) {
query.put("keyword", keyword);
}
if (chatType != null) {
query.put("chatType", chatType);
}
if (msgType != null) {
query.put("msgType", msgType);
}
if (startTime != null) {
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
}
if (endTime != null) {
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
}
query.put("page", String.valueOf(page));
query.put("size", String.valueOf(size));
ApiResponse<PageResult<ImMessage>> response = request(
"GET",
buildUri("/api/im/admin/messages/search", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<WebhookConfigView> listWebhookConfigs() {
ApiResponse<List<WebhookConfigView>> response = request(
"GET",
buildUri("/api/im/admin/webhooks", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public WebhookConfigView createWebhookConfig(String url, String secret, Boolean enabled) {
ApiResponse<WebhookConfigView> response = request(
"POST",
buildUri("/api/im/admin/webhooks", appQuery()),
new WebhookConfigRequest(url, secret, enabled),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public WebhookConfigView updateWebhookConfig(String id, String url, String secret, Boolean enabled) {
ApiResponse<WebhookConfigView> response = request(
"PUT",
buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()),
new WebhookConfigRequest(url, secret, enabled),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void deleteWebhookConfig(String id) {
request(
"DELETE",
buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public List<KeywordFilterView> listKeywordFilters() {
ApiResponse<List<KeywordFilterView>> response = request(
"GET",
buildUri("/api/im/admin/keyword-filters", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public KeywordFilterView createKeywordFilter(String pattern, String replacement, String action, Boolean enabled) {
ApiResponse<KeywordFilterView> response = request(
"POST",
buildUri("/api/im/admin/keyword-filters", appQuery()),
new KeywordFilterRequest(pattern, replacement, action, enabled),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public KeywordFilterView updateKeywordFilter(String id, String pattern, String replacement, String action, Boolean enabled) {
ApiResponse<KeywordFilterView> response = request(
"PUT",
buildUri("/api/im/admin/keyword-filters/" + encode(id), appQuery()),
new KeywordFilterRequest(pattern, replacement, action, enabled),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void deleteKeywordFilter(String id) {
request(
"DELETE",
buildUri("/api/im/admin/keyword-filters/" + encode(id), appQuery()),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public GlobalMuteView getGlobalMute() {
ApiResponse<GlobalMuteView> response = request(
"GET",
buildUri("/api/im/admin/global-mute", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GlobalMuteView setGlobalMute(boolean enabled) {
ApiResponse<GlobalMuteView> response = request(
"PUT",
buildUri("/api/im/admin/global-mute", Map.of("appId", appId, "enabled", String.valueOf(enabled))),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
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() {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/friends", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public FriendLinkView addFriend(String friendId) {
Map<String, String> query = appQuery();
query.put("friendId", friendId);
ApiResponse<FriendLinkView> response = request(
"POST",
buildUri("/api/im/friends", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void removeFriend(String friendId) {
Map<String, String> query = appQuery();
query.put("friendId", friendId);
request(
"DELETE",
buildUri("/api/im/friends/" + encode(friendId), query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public List<FriendRequestView> listFriendRequests(String direction) {
Map<String, String> query = appQuery();
if (direction != null) {
query.put("direction", direction);
}
ApiResponse<List<FriendRequestView>> response = request(
"GET",
buildUri("/api/im/friend-requests", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public FriendRequestView sendFriendRequest(String toUserId, String remark) {
Map<String, String> query = appQuery();
query.put("toUserId", toUserId);
if (remark != null) {
query.put("remark", remark);
}
ApiResponse<FriendRequestView> response = request(
"POST",
buildUri("/api/im/friend-requests", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public FriendRequestView acceptFriendRequest(String requestId) {
ApiResponse<FriendRequestView> response = request(
"POST",
buildUri("/api/im/friend-requests/" + encode(requestId) + "/accept", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public FriendRequestView rejectFriendRequest(String requestId) {
ApiResponse<FriendRequestView> response = request(
"POST",
buildUri("/api/im/friend-requests/" + encode(requestId) + "/reject", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<BlacklistView> listBlacklist() {
ApiResponse<List<BlacklistView>> response = request(
"GET",
buildUri("/api/im/blacklist", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public BlacklistView addBlacklist(String blockedUserId) {
Map<String, String> query = appQuery();
query.put("blockedUserId", blockedUserId);
ApiResponse<BlacklistView> response = request(
"POST",
buildUri("/api/im/blacklist", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void removeBlacklist(String blockedUserId) {
Map<String, String> query = appQuery();
query.put("blockedUserId", blockedUserId);
request(
"DELETE",
buildUri("/api/im/blacklist", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public List<GroupView> listGroups() {
ApiResponse<List<GroupView>> response = request(
"GET",
buildUri("/api/im/groups", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<GroupView> listPublicGroups(String keyword) {
Map<String, String> query = appQuery();
if (keyword != null) {
query.put("keyword", keyword);
}
ApiResponse<List<GroupView>> response = request(
"GET",
buildUri("/api/im/groups/public", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView getGroup(String groupId) {
ApiResponse<GroupView> response = request(
"GET",
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView createGroup(String name, List<String> memberIds, String groupType) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/groups", appQuery()),
new CreateGroupRequest(name, memberIds, groupType),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView updateGroup(String groupId, String name, String announcement) {
ApiResponse<GroupView> response = request(
"PUT",
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
new UpdateGroupRequest(name, announcement),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView addGroupMember(String groupId, String userId) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()),
new MemberRequest(userId),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView removeGroupMember(String groupId, String targetUserId) {
ApiResponse<GroupView> response = request(
"DELETE",
buildUri("/api/im/groups/" + encode(groupId) + "/members/" + encode(targetUserId), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView setGroupRole(String groupId, String userId, String role) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/roles", appQuery()),
new SetRoleRequest(userId, role),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView muteGroupMember(String groupId, String userId, long minutes) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/mute", appQuery()),
new MuteMemberRequest(userId, minutes),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void dismissGroup(String groupId) {
request(
"DELETE",
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public GroupJoinRequestView sendGroupJoinRequest(String groupId, String remark) {
Map<String, String> query = appQuery();
if (remark != null) {
query.put("remark", remark);
}
ApiResponse<GroupJoinRequestView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<GroupJoinRequestView> listGroupJoinRequests(String groupId) {
ApiResponse<List<GroupJoinRequestView>> response = request(
"GET",
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupJoinRequestView acceptGroupJoinRequest(String groupId, String requestId) {
ApiResponse<GroupJoinRequestView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/accept", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupJoinRequestView rejectGroupJoinRequest(String groupId, String requestId) {
ApiResponse<GroupJoinRequestView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/reject", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void setConversationPinned(String targetId, String chatType, boolean pinned) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
query.put("pinned", String.valueOf(pinned));
request(
"PUT",
buildUri("/api/im/conversations/" + encode(targetId) + "/pinned", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public void setConversationMuted(String targetId, String chatType, boolean muted) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
query.put("muted", String.valueOf(muted));
request(
"PUT",
buildUri("/api/im/conversations/" + encode(targetId) + "/muted", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public void markRead(String targetId, String chatType) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
request(
"PUT",
buildUri("/api/im/conversations/" + encode(targetId) + "/read", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public void setDraft(String targetId, String chatType, String draft) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
if (draft != null) {
query.put("draft", draft);
}
request(
"PUT",
buildUri("/api/im/conversations/" + encode(targetId) + "/draft", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public void deleteConversation(String targetId, String chatType) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
request(
"DELETE",
buildUri("/api/im/conversations/" + encode(targetId), query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public PageResult<ImMessage> queryAdminMessages(
String userA,
String userB,
String msgType,
String keyword,
LocalDateTime startTime,
LocalDateTime endTime,
int page,
int size) {
Map<String, String> query = appQuery();
query.put("userA", userA);
query.put("userB", userB);
if (msgType != null) {
query.put("msgType", msgType);
}
if (keyword != null) {
query.put("keyword", keyword);
}
if (startTime != null) {
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
}
if (endTime != null) {
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
}
query.put("page", String.valueOf(page));
query.put("size", String.valueOf(size));
ApiResponse<PageResult<ImMessage>> response = request(
"GET",
buildUri("/api/im/admin/messages", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public ImMessage revokeAdminMessage(String messageId) {
ApiResponse<ImMessage> response = request(
"POST",
buildUri("/api/im/admin/messages/" + encode(messageId) + "/revoke", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void dismissAdminGroup(String groupId) {
request(
"DELETE",
buildUri("/api/im/admin/groups/" + encode(groupId), appQuery()),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
private static String sha256Hex(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(normalize(value).getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
throw new ImSdkException("Failed to hash callback body", e);
}
}
private static String hmacSha256Hex(String secret, String payload) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
throw new ImSdkException("Failed to verify callback signature", e);
}
}
private static String normalize(String value) {
return value == null ? "" : value;
}
private static String text(JsonNode node, String field) {
JsonNode value = node == null ? null : node.get(field);
return value == null || value.isNull() ? null : value.asText();
}
private static long longValue(JsonNode node, String field) {
JsonNode value = node == null ? null : node.get(field);
if (value == null || value.isNull()) {
return 0L;
}
return value.asLong();
}
private Map<String, String> authorizedHeaders() {
String token = bearerTokenSupplier == null ? null : bearerTokenSupplier.get();
if (token == null || token.isBlank()) {
throw new ImSdkException("Bearer token is required for this call");
}
return Map.of("Authorization", "Bearer " + token);
}
private Map<String, String> publicHeaders() {
return Map.of();
}
private <T> ApiResponse<T> request(
String method,
URI uri,
Object body,
Map<String, String> headers,
TypeReference<ApiResponse<T>> responseType
) {
try {
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
.header("Content-Type", "application/json")
.headers(flatten(headers));
if (body != null) {
builder.method(method, HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)));
} else if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
builder.method(method, HttpRequest.BodyPublishers.noBody());
} else {
builder.method(method, HttpRequest.BodyPublishers.noBody());
}
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 <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) {
StringBuilder builder = new StringBuilder(baseUrl).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 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) {
Map<String, String> query = new LinkedHashMap<>();
query.put("appId", appId);
query.put("userId", userId);
query.put("timestamp", String.valueOf(timestamp));
query.put("nonce", nonce);
if (nickname != null && !nickname.isBlank()) {
query.put("nickname", nickname);
}
if (avatar != null && !avatar.isBlank()) {
query.put("avatar", avatar);
}
return query;
}
private Map<String, String> appQuery() {
Map<String, String> query = new LinkedHashMap<>();
query.put("appId", appId);
return query;
}
private Map<String, String> queryWithPage(int page, int size) {
Map<String, String> query = appQuery();
query.put("page", String.valueOf(page));
query.put("size", String.valueOf(size));
return query;
}
private Map<String, String> queryWithSize(String key, String value, int size) {
Map<String, String> query = appQuery();
query.put(key, value);
query.put("size", String.valueOf(size));
return query;
}
private Map<String, String> queryParams(String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size) {
Map<String, String> query = new LinkedHashMap<>();
query.put("appId", appId);
if (msgType != null && !msgType.isBlank()) {
query.put("msgType", msgType);
}
if (keyword != null && !keyword.isBlank()) {
query.put("keyword", keyword);
}
if (startTime != null) {
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
}
if (endTime != null) {
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
}
query.put("page", String.valueOf(page));
query.put("size", String.valueOf(size));
return query;
}
private String[] flatten(Map<String, String> headers) {
String[] pairs = new String[headers.size() * 2];
int index = 0;
for (Map.Entry<String, String> entry : headers.entrySet()) {
pairs[index++] = entry.getKey();
pairs[index++] = entry.getValue();
}
return pairs;
}
private String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private String trimTrailingSlash(String value) {
return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
}
public static final class Builder {
private String baseUrl;
private String pushBaseUrl;
private String updateBaseUrl;
private String appId;
private String appSecret;
private Supplier<String> bearerTokenSupplier;
private Builder() {}
public Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
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) {
this.appId = appId;
return this;
}
public Builder appSecret(String appSecret) {
this.appSecret = appSecret;
return this;
}
public Builder bearerTokenSupplier(Supplier<String> bearerTokenSupplier) {
this.bearerTokenSupplier = bearerTokenSupplier;
return this;
}
public XuqmImServerSdk build() {
return new XuqmImServerSdk(this);
}
}
public record LoginResponse(String token, long expiresAt) {}
public record ConversationView(
String targetId,
String chatType,
String lastMsgContent,
String lastMsgType,
Long lastMsgTime,
int unreadCount,
boolean isMuted,
boolean isPinned
) {}
public record AccountView(
String id,
String appId,
String userId,
String nickname,
String gender,
String avatar,
String status,
LocalDateTime createdAt
) {}
public record FriendLinkView(
Long id,
String appId,
String userId,
String friendId,
Instant createdAt
) {}
public record FriendRequestView(
String id,
String appId,
String fromUserId,
String toUserId,
String remark,
String status,
LocalDateTime createdAt,
LocalDateTime reviewedAt
) {}
public record BlacklistView(
String id,
String appId,
String userId,
String blockedUserId,
LocalDateTime createdAt
) {}
public record GroupView(
String id,
String appId,
String name,
String groupType,
String creatorId,
String memberIds,
String adminIds,
String announcement,
LocalDateTime createdAt
) {
public List<String> memberIdList() {
return parseJsonStringList(memberIds);
}
public List<String> adminIdList() {
return parseJsonStringList(adminIds);
}
}
public record GroupJoinRequestView(
String id,
String appId,
String groupId,
String requesterId,
String remark,
String status,
LocalDateTime createdAt,
LocalDateTime reviewedAt
) {}
public record StatsView(
long totalMessages,
long totalUsers,
long totalGroups,
long todayMessages
) {}
public record WebhookConfigView(
String id,
String appId,
String url,
String secret,
boolean enabled,
Long createdAt
) {}
public record KeywordFilterView(
String id,
String appId,
String pattern,
String replacement,
String action,
boolean enabled,
Long createdAt
) {}
public record GlobalMuteView(
String id,
String appId,
boolean enabled,
Long createdAt,
Long updatedAt
) {}
public record WebhookCallbackEnvelope(
String callbackId,
String callbackType,
String callbackEvent,
long requestTime,
JsonNode payload,
String signature,
String appId
) {}
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(
String msgType,
String keyword,
LocalDateTime startTime,
LocalDateTime endTime,
Integer page,
Integer size
) {
public HistoryQuery() {
this(null, null, null, null, null, null);
}
}
public record PageResult<T>(
List<T> content,
long totalElements,
int totalPages,
int size,
int number,
int numberOfElements,
boolean first,
boolean last,
boolean empty
) {}
public record ImMessage(
String id,
String appId,
String fromUserId,
String toId,
String chatType,
String msgType,
String content,
String status,
String mentionedUserIds,
Integer groupReadCount,
Long createdAt,
Long editedAt
) {}
public record SendMessageRequest(
String messageId,
String toId,
String chatType,
String msgType,
String content,
String mentionedUserIds
) {}
public record WebhookConfigRequest(
String url,
String secret,
Boolean enabled
) {}
public record KeywordFilterRequest(
String pattern,
String replacement,
String action,
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(
String name,
List<String> memberIds,
String groupType
) {}
public record UpdateGroupRequest(
String name,
String announcement
) {}
public record MemberRequest(String userId) {}
public record SetRoleRequest(String userId, String role) {}
public record MuteMemberRequest(String userId, long minutes) {}
public static final class ImSdkException extends RuntimeException {
public ImSdkException(String message) {
super(message);
}
public ImSdkException(String message, Throwable cause) {
super(message, cause);
}
}
private static List<String> parseJsonStringList(String value) {
if (value == null || value.isBlank()) {
return List.of();
}
String trimmed = value.trim();
if (!trimmed.startsWith("[")) {
return trimmed.isEmpty() ? List.of() : List.of(trimmed.split(","));
}
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(trimmed, new TypeReference<List<String>>() {});
} catch (IOException e) {
return new ArrayList<>(List.of(trimmed));
}
}
}