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 appKey; private final String appSecret; private final Supplier 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(builder.baseUrl); this.pushBaseUrl = trimTrailingSlash(builder.pushBaseUrl == null ? builder.baseUrl : builder.pushBaseUrl); this.updateBaseUrl = trimTrailingSlash(builder.updateBaseUrl == null ? builder.baseUrl : builder.updateBaseUrl); this.appKey = Objects.requireNonNull(builder.appKey, "appKey"); this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret"); this.bearerTokenSupplier = builder.bearerTokenSupplier; } public static Builder builder() { return new Builder(); } public ImMessage sendMessage(SendMessageRequest request) { ApiResponse response = request( "POST", buildUri("/api/im/messages/send", Map.of("appKey", appKey)), request, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public ImMessage revokeMessage(String messageId) { ApiResponse response = request( "POST", buildUri("/api/im/messages/" + encode(messageId) + "/revoke", Map.of("appKey", appKey)), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public ImMessage editMessage(String messageId, String content) { ApiResponse response = request( "PUT", buildUri("/api/im/messages/" + encode(messageId), Map.of("appKey", appKey)), Map.of("content", content), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public AccountView importAccount(String userId, String nickname, String avatar, String gender, String status) { ApiResponse response = request( "POST", buildUri("/api/im/accounts/import", appQuery()), new ImportAccountRequest(userId, nickname, avatar, gender, status), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List importAccounts(List accounts) { ApiResponse> response = request( "POST", buildUri("/api/im/accounts/import/batch", appQuery()), accounts, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void deleteAccount(String userId) { request( "DELETE", buildUri("/api/im/accounts/" + encode(userId), appQuery()), null, authorizedHeaders(), new TypeReference>() {} ); } public boolean checkAccount(String userId) { ApiResponse> response = request( "GET", buildUri("/api/im/accounts/" + encode(userId) + "/exists", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); Object exists = response.data().get("exists"); return exists instanceof Boolean b && b; } public List listConversations(int size) { ApiResponse> response = request( "GET", buildUri("/api/im/conversations", Map.of("appKey", appKey, "page", "0", "size", String.valueOf(size))), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public PageResult fetchHistory(String toId, HistoryQuery query) { HistoryQuery effective = query == null ? new HistoryQuery() : query; ApiResponse> 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 fetchGroupHistory(String groupId, HistoryQuery query) { HistoryQuery effective = query == null ? new HistoryQuery() : query; ApiResponse> 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 PageResult searchMessages(MessageSearchQuery query) { MessageSearchQuery effective = query == null ? new MessageSearchQuery() : query; ApiResponse> response = request( "GET", buildUri( "/api/im/messages/search", messageSearchQuery( effective.chatType(), 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 listWebhooks() { ApiResponse> response = request( "GET", buildUri("/api/im/admin/webhooks", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public WebhookConfigView createWebhook(WebhookConfigRequest request) { ApiResponse response = request( "POST", buildUri("/api/im/admin/webhooks", appQuery()), request, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public WebhookConfigView updateWebhook(String id, WebhookConfigRequest request) { ApiResponse response = request( "PUT", buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()), request, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void deleteWebhook(String id) { request( "DELETE", buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()), null, authorizedHeaders(), new TypeReference>() {} ); } public List listConversations() { return listConversations(20); } public AccountView getProfile(String userId) { ApiResponse 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 query = appQuery(); if (nickname != null) { query.put("nickname", nickname); } if (avatar != null) { query.put("avatar", avatar); } if (gender != null) { query.put("gender", gender); } ApiResponse response = request( "PUT", buildUri("/api/im/accounts/" + encode(userId), query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List searchAccounts(String keyword, int size) { ApiResponse> response = request( "GET", buildUri("/api/im/accounts/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 = appKey + "\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, "appKey") ); } catch (JsonProcessingException e) { throw new ImSdkException("Invalid callback body", e); } } public ImMessage parseMessageCallbackPayload(String body) { return parseMessageCallbackPayload(parseCallbackEnvelope(body)); } public ImMessage parseMessageCallbackPayload(WebhookCallbackEnvelope envelope) { if (!envelope.isMessageEvent() || !(envelope.isMessageSentEvent() || envelope.isMessageEditedEvent() || envelope.isMessageRevokedEvent())) { throw new ImSdkException("Callback event is not a message event"); } return readPayload(envelope, ImMessage.class); } public ImMessage parseMessageSentCallbackPayload(String body) { return parseMessageSentCallbackPayload(parseCallbackEnvelope(body)); } public ImMessage parseMessageSentCallbackPayload(WebhookCallbackEnvelope envelope) { requireMessageEvent(envelope, "message.sent"); return readPayload(envelope, ImMessage.class); } public ImMessage parseMessageEditedCallbackPayload(String body) { return parseMessageEditedCallbackPayload(parseCallbackEnvelope(body)); } public ImMessage parseMessageEditedCallbackPayload(WebhookCallbackEnvelope envelope) { requireMessageEvent(envelope, "message.edited"); return readPayload(envelope, ImMessage.class); } public ImMessage parseMessageRevokedCallbackPayload(String body) { return parseMessageRevokedCallbackPayload(parseCallbackEnvelope(body)); } public ImMessage parseMessageRevokedCallbackPayload(WebhookCallbackEnvelope envelope) { requireMessageEvent(envelope, "message.revoked"); return readPayload(envelope, ImMessage.class); } public MessageReadCallbackPayload parseMessageReadCallbackPayload(String body) { return parseMessageReadCallbackPayload(parseCallbackEnvelope(body)); } public MessageReadCallbackPayload parseMessageReadCallbackPayload(WebhookCallbackEnvelope envelope) { if (!envelope.isReadReceiptEvent()) { throw new ImSdkException("Callback event is not a message.read event"); } return readPayload(envelope, MessageReadCallbackPayload.class); } public void registerPushToken(String userId, String vendor, String token) { request( "POST", buildUri(pushBaseUrl, "/api/push/register", Map.of( "appKey", appKey, "userId", userId, "vendor", vendor, "token", token )), null, publicHeaders(), new TypeReference>() {} ); } public void sendPush(String userId, String title, String body, String payload) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); 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>() {} ); } public Map checkAppUpdate(String platform, int currentVersionCode) { ApiResponse> response = request( "GET", buildUri(updateBaseUrl, "/api/v1/updates/app/check", Map.of( "appKey", appKey, "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 form = new LinkedHashMap<>(); form.put("appKey", appKey); 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 response = multipartRequest( "POST", buildUri(updateBaseUrl, "/api/v1/updates/app/upload", Map.of()), form, "apkFile", apkFile, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public UnifiedReleaseResult uploadUnifiedRelease(UnifiedReleaseManifest manifest, Map files) { Map form = new LinkedHashMap<>(); form.put("appKey", appKey); try { form.put("manifest", objectMapper.writeValueAsString(manifest)); } catch (JsonProcessingException e) { throw new ImSdkException("Failed to serialize unified release manifest", e); } ApiResponse response = multipartRequest( "POST", buildUri(updateBaseUrl, "/api/v1/updates/unified/upload", Map.of()), form, files, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public AppVersionView publishAppVersion(String id) { ApiResponse 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 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 body = new LinkedHashMap<>(); body.put("enabled", enabled); body.put("percent", percent); ApiResponse response = request( "POST", buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/gray", Map.of()), body, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listAppVersions(String platform) { ApiResponse> response = request( "GET", buildUri(updateBaseUrl, "/api/v1/updates/app/list", Map.of("appKey", appKey, "platform", platform)), null, publicHeaders(), new TypeReference<>() {} ); return response.data(); } public RnBundleView checkRnUpdate(String moduleId, String platform, String currentVersion) { ApiResponse response = request( "GET", buildUri(updateBaseUrl, "/api/v1/rn/update/check", Map.of( "appKey", appKey, "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 form = new LinkedHashMap<>(); form.put("appKey", appKey); 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 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 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 response = request( "POST", buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/unpublish", Map.of()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listRnBundles(String moduleId, String platform) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); if (moduleId != null) { query.put("moduleId", moduleId); } if (platform != null) { query.put("platform", platform); } ApiResponse> response = request( "GET", buildUri(updateBaseUrl, "/api/v1/rn/list", query), null, publicHeaders(), new TypeReference<>() {} ); return response.data(); } public List listFriends() { ApiResponse> response = request( "GET", buildUri("/api/im/friends", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendLinkView addFriend(String friendId) { Map query = appQuery(); query.put("friendId", friendId); ApiResponse response = request( "POST", buildUri("/api/im/friends", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List addFriends(List friendIds) { ApiResponse> response = request( "POST", buildUri("/api/im/friends/batch", appQuery()), new FriendBatchRequest(friendIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void removeFriend(String friendId) { Map query = appQuery(); query.put("friendId", friendId); request( "DELETE", buildUri("/api/im/friends/" + encode(friendId), query), null, authorizedHeaders(), new TypeReference>() {} ); } public void removeFriends(List friendIds) { request( "POST", buildUri("/api/im/friends/batch/remove", appQuery()), new FriendBatchRequest(friendIds), authorizedHeaders(), new TypeReference>() {} ); } public void removeAllFriends() { request( "DELETE", buildUri("/api/im/friends", appQuery()), null, authorizedHeaders(), new TypeReference>() {} ); } public FriendLinkView setFriendGroup(String friendId, String groupName) { Map query = appQuery(); if (groupName != null) { query.put("groupName", groupName); } ApiResponse response = request( "PUT", buildUri("/api/im/friends/" + encode(friendId) + "/group", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listFriendGroups() { ApiResponse> response = request( "GET", buildUri("/api/im/friends/groups", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listFriendsByGroup(String groupName) { ApiResponse> response = request( "GET", buildUri("/api/im/friends/groups/" + encode(groupName), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listFriendRequests(String direction) { Map query = appQuery(); if (direction != null) { query.put("direction", direction); } ApiResponse> response = request( "GET", buildUri("/api/im/friend-requests", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendRequestView sendFriendRequest(String toUserId, String remark) { Map query = appQuery(); query.put("toUserId", toUserId); if (remark != null) { query.put("remark", remark); } ApiResponse response = request( "POST", buildUri("/api/im/friend-requests", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendRequestView acceptFriendRequest(String requestId) { ApiResponse 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 response = request( "POST", buildUri("/api/im/friend-requests/" + encode(requestId) + "/reject", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List acceptFriendRequests(List requestIds) { ApiResponse> response = request( "POST", buildUri("/api/im/friend-requests/batch/accept", appQuery()), new RequestBatch(requestIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List rejectFriendRequests(List requestIds) { ApiResponse> response = request( "POST", buildUri("/api/im/friend-requests/batch/reject", appQuery()), new RequestBatch(requestIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listBlacklist() { ApiResponse> response = request( "GET", buildUri("/api/im/blacklist", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public BlacklistView addBlacklist(String blockedUserId) { Map query = appQuery(); query.put("blockedUserId", blockedUserId); ApiResponse response = request( "POST", buildUri("/api/im/blacklist", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void removeBlacklist(String blockedUserId) { Map query = appQuery(); query.put("blockedUserId", blockedUserId); request( "DELETE", buildUri("/api/im/blacklist", query), null, authorizedHeaders(), new TypeReference>() {} ); } public BlacklistCheckResult checkBlacklist(String targetUserId) { Map query = appQuery(); query.put("targetUserId", targetUserId); ApiResponse response = request( "GET", buildUri("/api/im/blacklist/check", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listGroups() { ApiResponse> response = request( "GET", buildUri("/api/im/groups", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listPublicGroups(String keyword) { Map query = appQuery(); if (keyword != null) { query.put("keyword", keyword); } ApiResponse> response = request( "GET", buildUri("/api/im/groups/public", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List searchGroups(String keyword, int size) { ApiResponse> response = request( "GET", buildUri("/api/im/groups/search", queryWithSize("keyword", keyword, size)), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView getGroup(String groupId) { ApiResponse response = request( "GET", buildUri("/api/im/groups/" + encode(groupId), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listGroupMembers(String groupId) { ApiResponse> response = request( "GET", buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List searchGroupMembers(String groupId, String keyword, int size) { ApiResponse> response = request( "GET", buildUri("/api/im/groups/" + encode(groupId) + "/members/search", queryWithSize("keyword", keyword, size)), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView createGroup(String name, List memberIds, String groupType) { ApiResponse 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 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 response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()), new MemberRequest(userId), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView addGroupMembers(String groupId, List userIds) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/members/batch", appQuery()), new MemberBatchRequest(userIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView removeGroupMember(String groupId, String targetUserId) { ApiResponse response = request( "DELETE", buildUri("/api/im/groups/" + encode(groupId) + "/members/" + encode(targetUserId), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView removeGroupMembers(String groupId, List userIds) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/members/batch/remove", appQuery()), new MemberBatchRequest(userIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView setGroupRole(String groupId, String userId, String role) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/roles", appQuery()), new SetRoleRequest(userId, role), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView transferGroupOwner(String groupId, String newOwnerId) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/owner", appQuery()), new TransferOwnerRequest(newOwnerId), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void leaveGroup(String groupId) { request( "DELETE", buildUri("/api/im/groups/" + encode(groupId) + "/members/me", appQuery()), null, authorizedHeaders(), new TypeReference>() {} ); } public GroupView updateGroupAttributes(String groupId, Map attributes) { ApiResponse response = request( "PUT", buildUri("/api/im/groups/" + encode(groupId) + "/attributes", appQuery()), attributes == null ? Map.of() : attributes, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView removeGroupAttributes(String groupId, List keys) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/attributes/delete", appQuery()), new AttributeKeysRequest(keys), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView muteGroupMember(String groupId, String userId, long minutes) { ApiResponse 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>() {} ); } public GroupJoinRequestView sendGroupJoinRequest(String groupId, String remark) { Map query = appQuery(); if (remark != null) { query.put("remark", remark); } ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listGroupJoinRequests(String groupId) { ApiResponse> 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 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 response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/reject", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List acceptGroupJoinRequests(String groupId, List requestIds) { ApiResponse> response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/batch/accept", appQuery()), new RequestBatch(requestIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List rejectGroupJoinRequests(String groupId, List requestIds) { ApiResponse> response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/batch/reject", appQuery()), new RequestBatch(requestIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public Map queryUserState(List userIds) { Map query = new LinkedHashMap<>(); query.put("userIds", String.join(",", userIds)); ApiResponse> response = request( "GET", buildUri("/api/im/admin/users/state", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void kickUsers(String appKey, List userIds) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); request( "POST", buildUri("/api/im/admin/users/kick", query), Map.of("userIds", userIds), authorizedHeaders(), new TypeReference>() {} ); } public List batchSendMessage(String appKey, List toIds, String msgType, String content) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); ApiResponse> response = request( "POST", buildUri("/api/im/admin/messages/batch-send", query), Map.of("toIds", toIds, "msgType", msgType, "content", content), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void adminSetMsgRead(String appKey, String userId) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); request( "POST", buildUri("/api/im/admin/messages/read", query), Map.of("userId", userId), authorizedHeaders(), new TypeReference>() {} ); } public List importMessages(String appKey, List requests) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); ApiResponse> response = request( "POST", buildUri("/api/im/admin/messages/import", query), requests, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List checkFriends(String appKey, List friendIds) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); ApiResponse> response = request( "POST", buildUri("/api/im/friends/check", query), Map.of("friendIds", friendIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView modifyGroupMemberInfo(String groupId, String userId, String nickName, String role) { Map body = new LinkedHashMap<>(); if (nickName != null) { body.put("nickName", nickName); } if (role != null) { body.put("role", role); } ApiResponse response = request( "PUT", buildUri("/api/im/groups/" + encode(groupId) + "/members/" + encode(userId) + "/info", appQuery()), body, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView adminTransferGroupOwner(String groupId, String newOwnerId) { ApiResponse response = request( "POST", buildUri("/api/im/admin/groups/" + encode(groupId) + "/owner", appQuery()), new TransferOwnerRequest(newOwnerId), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView adminUpdateGroupAttributes(String groupId, Map attributes) { ApiResponse response = request( "PUT", buildUri("/api/im/admin/groups/" + encode(groupId) + "/attributes", appQuery()), attributes == null ? Map.of() : attributes, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView adminRemoveGroupAttributes(String groupId, List keys) { ApiResponse response = request( "POST", buildUri("/api/im/admin/groups/" + encode(groupId) + "/attributes/delete", appQuery()), new AttributeKeysRequest(keys), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List adminGroupReadReceipts(String groupId, List messageIds) { ApiResponse> response = request( "POST", buildUri("/api/im/admin/groups/" + encode(groupId) + "/read-receipts", appQuery()), new GroupReadReceiptRequest(messageIds), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void setConversationPinned(String targetId, String chatType, boolean pinned) { Map 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>() {} ); } public void setConversationMuted(String targetId, String chatType, boolean muted) { Map 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>() {} ); } public void markRead(String targetId, String chatType) { Map query = appQuery(); query.put("chatType", chatType); request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/read", query), null, authorizedHeaders(), new TypeReference>() {} ); } public void setDraft(String targetId, String chatType, String draft) { Map 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>() {} ); } public void setConversationHidden(String targetId, String chatType, boolean hidden) { Map query = appQuery(); query.put("chatType", chatType); query.put("hidden", String.valueOf(hidden)); request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/hidden", query), null, authorizedHeaders(), new TypeReference>() {} ); } public void setConversationGroup(String targetId, String chatType, String groupName) { Map query = appQuery(); query.put("chatType", chatType); if (groupName != null) { query.put("groupName", groupName); } request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/group", query), null, authorizedHeaders(), new TypeReference>() {} ); } public List listConversationGroups() { ApiResponse> response = request( "GET", buildUri("/api/im/conversation-groups", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listConversationGroupItems(String groupName) { ApiResponse> response = request( "GET", buildUri("/api/im/conversation-groups/" + encode(groupName), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void deleteConversation(String targetId, String chatType) { Map query = appQuery(); query.put("chatType", chatType); request( "DELETE", buildUri("/api/im/conversations/" + encode(targetId), query), null, authorizedHeaders(), new TypeReference>() {} ); } 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 T readPayload(WebhookCallbackEnvelope envelope, Class payloadType) { try { return objectMapper.treeToValue(envelope.payload(), payloadType); } catch (JsonProcessingException e) { throw new ImSdkException("Failed to parse callback payload", e); } } private void requireMessageEvent(WebhookCallbackEnvelope envelope, String event) { if (!envelope.isMessageEvent() || !envelope.isEvent(event)) { throw new ImSdkException("Callback event is not a " + event + " event"); } } 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 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 publicHeaders() { return Map.of(); } private ApiResponse request( String method, URI uri, Object body, Map headers, TypeReference> 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 response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body()); } ApiResponse 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 ApiResponse multipartRequest( String method, URI uri, Map formFields, String fileFieldName, Path file, Map headers, TypeReference> responseType ) { String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", ""); try { HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, fileFieldName, file); Map 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 response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body()); } ApiResponse 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 ApiResponse multipartRequest( String method, URI uri, Map formFields, Map files, Map headers, TypeReference> responseType ) { String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", ""); try { HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, files); Map 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 response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body()); } ApiResponse 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 formFields, String fileFieldName, Path file ) throws IOException { var byteArrays = new ArrayList(); Charset charset = StandardCharsets.UTF_8; for (Map.Entry 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 HttpRequest.BodyPublisher ofMultipartData( String boundary, Map formFields, Map files ) throws IOException { var byteArrays = new ArrayList(); Charset charset = StandardCharsets.UTF_8; for (Map.Entry 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)); } for (Map.Entry entry : files.entrySet()) { Path file = entry.getValue(); byteArrays.add(("--" + boundary + "\r\n").getBytes(charset)); byteArrays.add(("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"; 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 query) { StringBuilder builder = new StringBuilder(baseUrl).append(path); if (query != null && !query.isEmpty()) { builder.append('?'); boolean first = true; for (Map.Entry 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 query) { StringBuilder builder = new StringBuilder(trimTrailingSlash(base)).append(path); if (query != null && !query.isEmpty()) { builder.append('?'); boolean first = true; for (Map.Entry 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 appQuery() { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); return query; } private Map queryWithPage(int page, int size) { Map query = appQuery(); query.put("page", String.valueOf(page)); query.put("size", String.valueOf(size)); return query; } private Map queryWithSize(String key, String value, int size) { Map query = appQuery(); query.put(key, value); query.put("size", String.valueOf(size)); return query; } private Map queryParams(String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); 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 Map messageSearchQuery( String chatType, String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size ) { Map query = new LinkedHashMap<>(); query.put("appKey", appKey); if (chatType != null && !chatType.isBlank()) { query.put("chatType", chatType); } 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 headers) { String[] pairs = new String[headers.size() * 2]; int index = 0; for (Map.Entry 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; } private static final String DEFAULT_BASE_URL = "https://dev.xuqinmin.com"; public static final class Builder { private String baseUrl = DEFAULT_BASE_URL; private String pushBaseUrl; private String updateBaseUrl; private String appKey; private String appSecret; private Supplier 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 appKey(String appKey) { this.appKey = appKey; return this; } public Builder appSecret(String appSecret) { this.appSecret = appSecret; return this; } public Builder bearerTokenSupplier(Supplier bearerTokenSupplier) { this.bearerTokenSupplier = bearerTokenSupplier; return this; } public XuqmImServerSdk build() { Objects.requireNonNull(this.appKey, "appKey is required"); Objects.requireNonNull(this.appSecret, "appSecret is required"); return new XuqmImServerSdk(this); } } public record LoginResponse(String token) {} public record ConversationView( String targetId, String chatType, String lastMsgContent, String lastMsgType, Long lastMsgTime, int unreadCount, boolean isMuted, boolean isPinned, String conversationGroup ) {} public record ConversationGroupItem( String targetId, String chatType, String groupName ) {} public record AccountView( String id, String appKey, String userId, String nickname, String gender, String avatar, String status, LocalDateTime createdAt ) {} public record ImportAccountRequest( String userId, String nickname, String avatar, String gender, String status ) {} public record FriendLinkView( Long id, String appKey, String userId, String friendId, String friendGroup, Instant createdAt ) {} public record FriendBatchRequest(List friendIds) {} public record FriendRequestView( String id, String appKey, String fromUserId, String toUserId, String remark, String status, LocalDateTime createdAt, LocalDateTime reviewedAt ) {} public record BlacklistView( String id, String appKey, String userId, String blockedUserId, LocalDateTime createdAt ) {} public record BlacklistCheckResult( String targetUserId, boolean blockedByMe, boolean blockedByTarget, boolean eitherBlocked ) {} public record GroupView( String id, String appKey, String name, String groupType, String creatorId, String memberIds, String adminIds, String announcement, String memberInfo, String extAttributes, LocalDateTime createdAt ) { public List memberIdList() { return parseJsonStringList(memberIds); } public List adminIdList() { return parseJsonStringList(adminIds); } } public record GroupReadReceiptSummary( String messageId, String groupId, int memberCount, int readCount, int unreadCount ) {} public record GroupJoinRequestView( String id, String appKey, String groupId, String requesterId, String remark, String status, LocalDateTime createdAt, LocalDateTime reviewedAt ) {} public record WebhookCallbackEnvelope( String callbackId, String callbackType, String callbackEvent, long requestTime, JsonNode payload, String signature, String appKey ) { public boolean isType(String type) { return type != null && callbackType != null && callbackType.equalsIgnoreCase(type); } public boolean isEvent(String event) { return event != null && callbackEvent != null && callbackEvent.equalsIgnoreCase(event); } public boolean isMessageEvent() { return isType("message"); } public boolean isMessageSentEvent() { return isEvent("message.sent"); } public boolean isMessageEditedEvent() { return isEvent("message.edited"); } public boolean isMessageRevokedEvent() { return isEvent("message.revoked"); } public boolean isReadReceiptEvent() { return isEvent("message.read"); } } public record AppVersionView( String id, String appKey, 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 appKey, 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 MessageSearchQuery( String chatType, String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, Integer page, Integer size ) { public MessageSearchQuery() { this(null, null, null, null, null, null, null); } } public record PageResult( List content, long totalElements, int totalPages, int size, int number, int numberOfElements, boolean first, boolean last, boolean empty ) {} public record ImMessage( String id, String appKey, 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 WebhookConfigView( String id, String appKey, String url, String secret, boolean enabled, LocalDateTime createdAt ) {} public record KeywordFilterRequest( String pattern, String replacement, String action, Boolean enabled ) {} public record AppVersionUploadRequest( String appKey, String platform, String versionName, int versionCode, String changeLog, boolean forceUpdate ) {} public record RnBundleUploadRequest( String appKey, String moduleId, String platform, String version, String minCommonVersion, String note ) {} public record MessageReadCallbackPayload( String appKey, String readerId, String peerId, String groupId, String chatType, long readAt, List messageIds ) {} public record UnifiedReleaseManifest( List appVersions, List rnBundles ) {} public record AppUploadItem( String fileKey, String platform, String versionName, int versionCode, String changeLog, boolean forceUpdate, String appStoreUrl, String marketUrl ) {} public record RnBundleUploadItem( String fileKey, String moduleId, String platform, String version, String minCommonVersion, String note ) {} public record UnifiedReleaseResult( List appVersions, List rnBundles ) {} public record CreateGroupRequest( String name, List memberIds, String groupType ) {} public record UpdateGroupRequest( String name, String announcement ) {} public record MemberRequest(String userId) {} public record MemberBatchRequest(List userIds) {} public record SetRoleRequest(String userId, String role) {} public record TransferOwnerRequest(String newOwnerId) {} public record AttributeKeysRequest(List keys) {} public record MuteMemberRequest(String userId, long minutes) {} public record GroupReadReceiptRequest(List messageIds) {} public record RequestBatch(List requestIds) {} public record FriendCheckResult(String userId, boolean isFriend) {} public record ImportMessageRequest( String messageId, String fromUserId, String toId, String chatType, String msgType, String content, String status, String createdAt ) {} 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 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>() {}); } catch (IOException e) { return new ArrayList<>(List.of(trimmed)); } } }