XuqmGroup-Server/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java
2026-05-07 19:39:42 +08:00

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;
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);
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",
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",
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",
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",
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;
}
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<ApiResponse<Void>>() {}
);
}
public void sendPush(String userId, String title, String body, String payload) {
Map<String, String> 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<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(
"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<>();
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<>();
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",
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(
"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<>();
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<>();
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();
}
public void kickUsers(String appKey, List<String> userIds) {
Map<String, String> query = new LinkedHashMap<>();
query.put("appKey", appKey);
request(
"POST",
buildUri("/api/im/admin/users/kick", query),
Map.of("userIds", userIds),
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public List<ImMessage> batchSendMessage(String appKey, List<String> toIds, String msgType, String content) {
Map<String, String> query = new LinkedHashMap<>();
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();
}
public void adminSetMsgRead(String appKey, String userId) {
Map<String, String> query = new LinkedHashMap<>();
query.put("appKey", appKey);
request(
"POST",
buildUri("/api/im/admin/messages/read", query),
Map.of("userId", userId),
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public List<ImMessage> importMessages(String appKey, List<ImportMessageRequest> requests) {
Map<String, String> query = new LinkedHashMap<>();
query.put("appKey", appKey);
ApiResponse<List<ImMessage>> response = request(
"POST",
buildUri("/api/im/admin/messages/import", query),
requests,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<FriendCheckResult> checkFriends(String appKey, List<String> friendIds) {
Map<String, String> query = new LinkedHashMap<>();
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<>();
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<>();
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<>();
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;
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;
}
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() {
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<String> 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<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,
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<T>(
List<T> content,
long totalElements,
int totalPages,
int size,
int number,
int numberOfElements,
boolean first,
boolean last,
boolean empty
) {}
public record ImMessage(
String id,
String 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<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));
}
}
}