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

2184 行
76 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;
2026-05-07 19:39:42 +08:00
private final String appKey;
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(builder.baseUrl);
this.pushBaseUrl = trimTrailingSlash(builder.pushBaseUrl == null ? builder.baseUrl : builder.pushBaseUrl);
this.updateBaseUrl = trimTrailingSlash(builder.updateBaseUrl == null ? builder.baseUrl : builder.updateBaseUrl);
2026-05-07 19:39:42 +08:00
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<ImMessage> response = request(
"POST",
2026-05-07 19:39:42 +08:00
buildUri("/api/im/messages/send", Map.of("appKey", appKey)),
request,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public ImMessage revokeMessage(String messageId) {
ApiResponse<ImMessage> response = request(
"POST",
2026-05-07 19:39:42 +08:00
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<ImMessage> response = request(
"PUT",
2026-05-07 19:39:42 +08:00
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<AccountView> response = request(
"POST",
buildUri("/api/im/accounts/import", appQuery()),
new ImportAccountRequest(userId, nickname, avatar, gender, status),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<AccountView> importAccounts(List<ImportAccountRequest> accounts) {
ApiResponse<List<AccountView>> 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<ApiResponse<Void>>() {}
);
}
public boolean checkAccount(String userId) {
ApiResponse<Map<String, Object>> 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<ConversationView> listConversations(int size) {
ApiResponse<List<ConversationView>> response = request(
"GET",
2026-05-07 19:39:42 +08:00
buildUri("/api/im/conversations", Map.of("appKey", appKey, "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 PageResult<ImMessage> searchMessages(MessageSearchQuery query) {
MessageSearchQuery effective = query == null ? new MessageSearchQuery() : query;
ApiResponse<PageResult<ImMessage>> 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<WebhookConfigView> listWebhooks() {
ApiResponse<List<WebhookConfigView>> response = request(
"GET",
buildUri("/api/im/admin/webhooks", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public WebhookConfigView createWebhook(WebhookConfigRequest request) {
ApiResponse<WebhookConfigView> response = request(
"POST",
buildUri("/api/im/admin/webhooks", appQuery()),
request,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public WebhookConfigView updateWebhook(String id, WebhookConfigRequest request) {
ApiResponse<WebhookConfigView> 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<ApiResponse<Void>>() {}
);
}
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 List<AccountView> searchAccounts(String keyword, int size) {
ApiResponse<List<AccountView>> 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;
}
2026-05-07 19:39:42 +08:00
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"),
2026-05-07 19:39:42 +08:00
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(
2026-05-07 19:39:42 +08:00
"appKey", appKey,
"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<>();
2026-05-07 19:39:42 +08:00
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<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(
2026-05-07 19:39:42 +08:00
"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<String, String> form = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
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<AppVersionView> 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<String, Path> files) {
Map<String, String> form = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
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<UnifiedReleaseResult> 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<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",
2026-05-07 19:39:42 +08:00
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<RnBundleView> response = request(
"GET",
buildUri(updateBaseUrl, "/api/v1/rn/update/check", Map.of(
2026-05-07 19:39:42 +08:00
"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<String, String> form = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
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<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<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
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 List<FriendLinkView> addFriends(List<String> friendIds) {
ApiResponse<List<FriendLinkView>> response = request(
"POST",
buildUri("/api/im/friends/batch", appQuery()),
new FriendBatchRequest(friendIds),
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 void removeFriends(List<String> friendIds) {
request(
"POST",
buildUri("/api/im/friends/batch/remove", appQuery()),
new FriendBatchRequest(friendIds),
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public void removeAllFriends() {
request(
"DELETE",
buildUri("/api/im/friends", appQuery()),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public FriendLinkView setFriendGroup(String friendId, String groupName) {
Map<String, String> query = appQuery();
if (groupName != null) {
query.put("groupName", groupName);
}
ApiResponse<FriendLinkView> response = request(
"PUT",
buildUri("/api/im/friends/" + encode(friendId) + "/group", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<String> listFriendGroups() {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/friends/groups", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<String> listFriendsByGroup(String groupName) {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/friends/groups/" + encode(groupName), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
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<FriendRequestView> acceptFriendRequests(List<String> requestIds) {
ApiResponse<List<FriendRequestView>> response = request(
"POST",
buildUri("/api/im/friend-requests/batch/accept", appQuery()),
new RequestBatch(requestIds),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<FriendRequestView> rejectFriendRequests(List<String> requestIds) {
ApiResponse<List<FriendRequestView>> response = request(
"POST",
buildUri("/api/im/friend-requests/batch/reject", appQuery()),
new RequestBatch(requestIds),
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 BlacklistCheckResult checkBlacklist(String targetUserId) {
Map<String, String> query = appQuery();
query.put("targetUserId", targetUserId);
ApiResponse<BlacklistCheckResult> response = request(
"GET",
buildUri("/api/im/blacklist/check", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
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 List<GroupView> searchGroups(String keyword, int size) {
ApiResponse<List<GroupView>> 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<GroupView> response = request(
"GET",
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<AccountView> listGroupMembers(String groupId) {
ApiResponse<List<AccountView>> response = request(
"GET",
buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<AccountView> searchGroupMembers(String groupId, String keyword, int size) {
ApiResponse<List<AccountView>> 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<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 addGroupMembers(String groupId, List<String> userIds) {
ApiResponse<GroupView> 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<GroupView> 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<String> userIds) {
ApiResponse<GroupView> 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<GroupView> 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<GroupView> 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<ApiResponse<Void>>() {}
);
}
public GroupView updateGroupAttributes(String groupId, Map<String, Object> attributes) {
ApiResponse<GroupView> 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<String> keys) {
ApiResponse<GroupView> 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<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 List<GroupJoinRequestView> acceptGroupJoinRequests(String groupId, List<String> requestIds) {
ApiResponse<List<GroupJoinRequestView>> 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<GroupJoinRequestView> rejectGroupJoinRequests(String groupId, List<String> requestIds) {
ApiResponse<List<GroupJoinRequestView>> 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<String, Boolean> queryUserState(List<String> userIds) {
Map<String, String> query = new LinkedHashMap<>();
query.put("userIds", String.join(",", userIds));
ApiResponse<Map<String, Boolean>> response = request(
"GET",
buildUri("/api/im/admin/users/state", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
2026-05-07 19:39:42 +08:00
public void kickUsers(String appKey, List<String> userIds) {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
request(
"POST",
buildUri("/api/im/admin/users/kick", query),
Map.of("userIds", userIds),
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
2026-05-07 19:39:42 +08:00
public List<ImMessage> batchSendMessage(String appKey, List<String> toIds, String msgType, String content) {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
ApiResponse<List<ImMessage>> 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();
}
2026-05-07 19:39:42 +08:00
public void adminSetMsgRead(String appKey, String userId) {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
request(
"POST",
buildUri("/api/im/admin/messages/read", query),
Map.of("userId", userId),
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
2026-05-07 19:39:42 +08:00
public List<ImMessage> importMessages(String appKey, List<ImportMessageRequest> requests) {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
ApiResponse<List<ImMessage>> response = request(
"POST",
buildUri("/api/im/admin/messages/import", query),
requests,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
2026-05-07 19:39:42 +08:00
public List<FriendCheckResult> checkFriends(String appKey, List<String> friendIds) {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
ApiResponse<List<FriendCheckResult>> 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<String, String> body = new LinkedHashMap<>();
if (nickName != null) {
body.put("nickName", nickName);
}
if (role != null) {
body.put("role", role);
}
ApiResponse<GroupView> 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<GroupView> 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<String, Object> attributes) {
ApiResponse<GroupView> 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<String> keys) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/admin/groups/" + encode(groupId) + "/attributes/delete", appQuery()),
new AttributeKeysRequest(keys),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<GroupReadReceiptSummary> adminGroupReadReceipts(String groupId, List<String> messageIds) {
ApiResponse<List<GroupReadReceiptSummary>> 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<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 setConversationHidden(String targetId, String chatType, boolean hidden) {
Map<String, String> 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<ApiResponse<Void>>() {}
);
}
public void setConversationGroup(String targetId, String chatType, String groupName) {
Map<String, String> 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<ApiResponse<Void>>() {}
);
}
public List<String> listConversationGroups() {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/conversation-groups", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<ConversationGroupItem> listConversationGroupItems(String groupName) {
ApiResponse<List<ConversationGroupItem>> 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<String, String> query = appQuery();
query.put("chatType", chatType);
request(
"DELETE",
buildUri("/api/im/conversations/" + encode(targetId), query),
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 <T> T readPayload(WebhookCallbackEnvelope envelope, Class<T> 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<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 <T> ApiResponse<T> multipartRequest(
String method,
URI uri,
Map<String, String> formFields,
Map<String, Path> files,
Map<String, String> headers,
TypeReference<ApiResponse<T>> responseType
) {
String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", "");
try {
HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, files);
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 HttpRequest.BodyPublisher ofMultipartData(
String boundary,
Map<String, String> formFields,
Map<String, Path> files
) 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));
}
for (Map.Entry<String, Path> 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<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> appQuery() {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
query.put("appKey", appKey);
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<>();
2026-05-07 19:39:42 +08:00
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<String, String> messageSearchQuery(
String chatType,
String msgType,
String keyword,
LocalDateTime startTime,
LocalDateTime endTime,
int page,
int size
) {
Map<String, String> query = new LinkedHashMap<>();
2026-05-07 19:39:42 +08:00
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<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;
}
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;
2026-05-07 19:39:42 +08:00
private String appKey;
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;
}
2026-05-07 19:39:42 +08:00
public Builder appKey(String appKey) {
this.appKey = appKey;
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() {
2026-05-07 19:39:42 +08:00
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,
2026-05-07 19:39:42 +08:00
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,
2026-05-07 19:39:42 +08:00
String appKey,
String userId,
String friendId,
String friendGroup,
Instant createdAt
) {}
public record FriendBatchRequest(List<String> friendIds) {}
public record FriendRequestView(
String id,
2026-05-07 19:39:42 +08:00
String appKey,
String fromUserId,
String toUserId,
String remark,
String status,
LocalDateTime createdAt,
LocalDateTime reviewedAt
) {}
public record BlacklistView(
String id,
2026-05-07 19:39:42 +08:00
String appKey,
String userId,
String blockedUserId,
LocalDateTime createdAt
) {}
public record BlacklistCheckResult(
String targetUserId,
boolean blockedByMe,
boolean blockedByTarget,
boolean eitherBlocked
) {}
public record GroupView(
String id,
2026-05-07 19:39:42 +08:00
String appKey,
String name,
String groupType,
String creatorId,
String memberIds,
String adminIds,
String announcement,
String memberInfo,
String extAttributes,
LocalDateTime createdAt
) {
public List<String> memberIdList() {
return parseJsonStringList(memberIds);
}
public List<String> adminIdList() {
return parseJsonStringList(adminIds);
}
}
public record GroupReadReceiptSummary(
String messageId,
String groupId,
int memberCount,
int readCount,
int unreadCount
) {}
public record GroupJoinRequestView(
String id,
2026-05-07 19:39:42 +08:00
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,
2026-05-07 19:39:42 +08:00
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,
2026-05-07 19:39:42 +08:00
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,
2026-05-07 19:39:42 +08:00
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<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,
2026-05-07 19:39:42 +08:00
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,
2026-05-07 19:39:42 +08:00
String appKey,
String url,
String secret,
boolean enabled,
LocalDateTime createdAt
) {}
public record KeywordFilterRequest(
String pattern,
String replacement,
String action,
Boolean enabled
) {}
public record AppVersionUploadRequest(
2026-05-07 19:39:42 +08:00
String appKey,
String platform,
String versionName,
int versionCode,
String changeLog,
boolean forceUpdate
) {}
public record RnBundleUploadRequest(
2026-05-07 19:39:42 +08:00
String appKey,
String moduleId,
String platform,
String version,
String minCommonVersion,
String note
) {}
public record MessageReadCallbackPayload(
2026-05-07 19:39:42 +08:00
String appKey,
String readerId,
String peerId,
String groupId,
String chatType,
long readAt,
List<String> messageIds
) {}
public record UnifiedReleaseManifest(
List<AppUploadItem> appVersions,
List<RnBundleUploadItem> 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<AppVersionView> appVersions,
List<RnBundleView> rnBundles
) {}
public record CreateGroupRequest(
String name,
List<String> memberIds,
String groupType
) {}
public record UpdateGroupRequest(
String name,
String announcement
) {}
public record MemberRequest(String userId) {}
public record MemberBatchRequest(List<String> userIds) {}
public record SetRoleRequest(String userId, String role) {}
public record TransferOwnerRequest(String newOwnerId) {}
public record AttributeKeysRequest(List<String> keys) {}
public record MuteMemberRequest(String userId, long minutes) {}
public record GroupReadReceiptRequest(List<String> messageIds) {}
public record RequestBatch(List<String> 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<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));
}
}
}