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.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.StandardCharsets; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.function.Supplier; public final class XuqmImServerSdk { private final HttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; private final String appId; private final String appSecret; private final Supplier bearerTokenSupplier; private XuqmImServerSdk(Builder builder) { this.httpClient = HttpClient.newHttpClient(); this.objectMapper = new ObjectMapper() .registerModule(new JavaTimeModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); this.baseUrl = trimTrailingSlash(Objects.requireNonNull(builder.baseUrl, "baseUrl")); this.appId = Objects.requireNonNull(builder.appId, "appId"); this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret"); this.bearerTokenSupplier = builder.bearerTokenSupplier; } public static Builder builder() { return new Builder(); } public LoginResponse login(String userId, String nickname, String avatar) { long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString().replace("-", ""); String payload = AppRequestSignatureUtil.payload(appId, userId, nickname, avatar, timestamp, nonce); String signature = AppRequestSignatureUtil.sign(appSecret, payload); URI uri = buildUri( "/api/im/auth/login", loginQuery(userId, nickname, avatar, timestamp, nonce) ); ApiResponse response = request( "POST", uri, null, Map.of( "X-App-Id", appId, "X-App-Timestamp", String.valueOf(timestamp), "X-App-Nonce", nonce, "X-App-Signature", signature ), new TypeReference<>() {} ); return response.data(); } public ImMessage sendMessage(SendMessageRequest request) { ApiResponse response = request( "POST", buildUri("/api/im/messages/send", Map.of("appId", appId)), request, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public ImMessage revokeMessage(String messageId) { ApiResponse response = request( "POST", buildUri("/api/im/messages/" + encode(messageId) + "/revoke", Map.of("appId", appId)), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listConversations(int size) { ApiResponse> response = request( "GET", buildUri("/api/im/conversations", Map.of("appId", appId, "page", "0", "size", String.valueOf(size))), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public PageResult fetchHistory(String toId, HistoryQuery query) { HistoryQuery effective = query == null ? new HistoryQuery() : query; ApiResponse> response = request( "GET", buildUri( "/api/im/messages/history/" + encode(toId), queryParams( effective.msgType(), effective.keyword(), effective.startTime(), effective.endTime(), effective.page() == null ? 0 : effective.page(), effective.size() == null ? 20 : effective.size() ) ), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public PageResult fetchGroupHistory(String groupId, HistoryQuery query) { HistoryQuery effective = query == null ? new HistoryQuery() : query; ApiResponse> response = request( "GET", buildUri( "/api/im/messages/group-history/" + encode(groupId), queryParams( effective.msgType(), effective.keyword(), effective.startTime(), effective.endTime(), effective.page() == null ? 0 : effective.page(), effective.size() == null ? 20 : effective.size() ) ), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listConversations() { return listConversations(20); } public AccountView getProfile(String userId) { ApiResponse response = request( "GET", buildUri("/api/im/accounts/" + encode(userId), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public AccountView updateProfile(String userId, String nickname, String avatar, String gender) { Map query = appQuery(); if (nickname != null) { query.put("nickname", nickname); } if (avatar != null) { query.put("avatar", avatar); } if (gender != null) { query.put("gender", gender); } ApiResponse response = request( "PUT", buildUri("/api/im/accounts/" + encode(userId), query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public PageResult listUsers(int page, int size) { ApiResponse> response = request( "GET", buildUri("/api/im/admin/users", queryWithPage(page, size)), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public AccountView registerUser(String userId, String nickname, String avatar) { Map body = new LinkedHashMap<>(); body.put("userId", userId); if (nickname != null) { body.put("nickname", nickname); } if (avatar != null) { body.put("avatar", avatar); } ApiResponse response = request( "POST", buildUri("/api/im/admin/users", appQuery()), body, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public AccountView updateUserStatus(String userId, String status) { ApiResponse response = request( "PUT", buildUri("/api/im/admin/users/" + encode(userId) + "/status", appQuery()), Map.of("status", status), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List searchUsers(String keyword, int size) { ApiResponse> response = request( "GET", buildUri("/api/im/admin/users/search", queryWithSize("keyword", keyword, size)), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public StatsView stats() { ApiResponse response = request( "GET", buildUri("/api/im/admin/stats", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listFriends() { ApiResponse> response = request( "GET", buildUri("/api/im/friends", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendLinkView addFriend(String friendId) { Map query = appQuery(); query.put("friendId", friendId); ApiResponse response = request( "POST", buildUri("/api/im/friends", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void removeFriend(String friendId) { Map query = appQuery(); query.put("friendId", friendId); request( "DELETE", buildUri("/api/im/friends/" + encode(friendId), query), null, authorizedHeaders(), new TypeReference>() {} ); } public List listFriendRequests(String direction) { Map query = appQuery(); if (direction != null) { query.put("direction", direction); } ApiResponse> response = request( "GET", buildUri("/api/im/friend-requests", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendRequestView sendFriendRequest(String toUserId, String remark) { Map query = appQuery(); query.put("toUserId", toUserId); if (remark != null) { query.put("remark", remark); } ApiResponse response = request( "POST", buildUri("/api/im/friend-requests", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendRequestView acceptFriendRequest(String requestId) { ApiResponse response = request( "POST", buildUri("/api/im/friend-requests/" + encode(requestId) + "/accept", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public FriendRequestView rejectFriendRequest(String requestId) { ApiResponse response = request( "POST", buildUri("/api/im/friend-requests/" + encode(requestId) + "/reject", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listBlacklist() { ApiResponse> response = request( "GET", buildUri("/api/im/blacklist", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public BlacklistView addBlacklist(String blockedUserId) { Map query = appQuery(); query.put("blockedUserId", blockedUserId); ApiResponse response = request( "POST", buildUri("/api/im/blacklist", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void removeBlacklist(String blockedUserId) { Map query = appQuery(); query.put("blockedUserId", blockedUserId); request( "DELETE", buildUri("/api/im/blacklist", query), null, authorizedHeaders(), new TypeReference>() {} ); } public List listGroups() { ApiResponse> response = request( "GET", buildUri("/api/im/groups", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listPublicGroups(String keyword) { Map query = appQuery(); if (keyword != null) { query.put("keyword", keyword); } ApiResponse> response = request( "GET", buildUri("/api/im/groups/public", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView getGroup(String groupId) { ApiResponse response = request( "GET", buildUri("/api/im/groups/" + encode(groupId), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView createGroup(String name, List memberIds, String groupType) { ApiResponse response = request( "POST", buildUri("/api/im/groups", appQuery()), new CreateGroupRequest(name, memberIds, groupType), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView updateGroup(String groupId, String name, String announcement) { ApiResponse response = request( "PUT", buildUri("/api/im/groups/" + encode(groupId), appQuery()), new UpdateGroupRequest(name, announcement), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView addGroupMember(String groupId, String userId) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()), new MemberRequest(userId), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView removeGroupMember(String groupId, String targetUserId) { ApiResponse response = request( "DELETE", buildUri("/api/im/groups/" + encode(groupId) + "/members/" + encode(targetUserId), appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView setGroupRole(String groupId, String userId, String role) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/roles", appQuery()), new SetRoleRequest(userId, role), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupView muteGroupMember(String groupId, String userId, long minutes) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/mute", appQuery()), new MuteMemberRequest(userId, minutes), authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void dismissGroup(String groupId) { request( "DELETE", buildUri("/api/im/groups/" + encode(groupId), appQuery()), null, authorizedHeaders(), new TypeReference>() {} ); } public GroupJoinRequestView sendGroupJoinRequest(String groupId, String remark) { Map query = appQuery(); if (remark != null) { query.put("remark", remark); } ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public List listGroupJoinRequests(String groupId) { ApiResponse> response = request( "GET", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupJoinRequestView acceptGroupJoinRequest(String groupId, String requestId) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/accept", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public GroupJoinRequestView rejectGroupJoinRequest(String groupId, String requestId) { ApiResponse response = request( "POST", buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/reject", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void setConversationPinned(String targetId, String chatType, boolean pinned) { Map query = appQuery(); query.put("chatType", chatType); query.put("pinned", String.valueOf(pinned)); request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/pinned", query), null, authorizedHeaders(), new TypeReference>() {} ); } public void setConversationMuted(String targetId, String chatType, boolean muted) { Map query = appQuery(); query.put("chatType", chatType); query.put("muted", String.valueOf(muted)); request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/muted", query), null, authorizedHeaders(), new TypeReference>() {} ); } public void markRead(String targetId, String chatType) { Map query = appQuery(); query.put("chatType", chatType); request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/read", query), null, authorizedHeaders(), new TypeReference>() {} ); } public void setDraft(String targetId, String chatType, String draft) { Map query = appQuery(); query.put("chatType", chatType); if (draft != null) { query.put("draft", draft); } request( "PUT", buildUri("/api/im/conversations/" + encode(targetId) + "/draft", query), null, authorizedHeaders(), new TypeReference>() {} ); } public void deleteConversation(String targetId, String chatType) { Map query = appQuery(); query.put("chatType", chatType); request( "DELETE", buildUri("/api/im/conversations/" + encode(targetId), query), null, authorizedHeaders(), new TypeReference>() {} ); } public PageResult queryAdminMessages( String userA, String userB, String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size) { Map query = appQuery(); query.put("userA", userA); query.put("userB", userB); if (msgType != null) { query.put("msgType", msgType); } if (keyword != null) { query.put("keyword", keyword); } if (startTime != null) { query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString()); } if (endTime != null) { query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString()); } query.put("page", String.valueOf(page)); query.put("size", String.valueOf(size)); ApiResponse> response = request( "GET", buildUri("/api/im/admin/messages", query), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public ImMessage revokeAdminMessage(String messageId) { ApiResponse response = request( "POST", buildUri("/api/im/admin/messages/" + encode(messageId) + "/revoke", appQuery()), null, authorizedHeaders(), new TypeReference<>() {} ); return response.data(); } public void dismissAdminGroup(String groupId) { request( "DELETE", buildUri("/api/im/admin/groups/" + encode(groupId), appQuery()), null, authorizedHeaders(), new TypeReference>() {} ); } private Map authorizedHeaders() { String token = bearerTokenSupplier == null ? null : bearerTokenSupplier.get(); if (token == null || token.isBlank()) { throw new ImSdkException("Bearer token is required for this call"); } return Map.of("Authorization", "Bearer " + token); } private ApiResponse request( String method, URI uri, Object body, Map headers, TypeReference> responseType ) { try { HttpRequest.Builder builder = HttpRequest.newBuilder(uri) .header("Content-Type", "application/json") .headers(flatten(headers)); if (body != null) { builder.method(method, HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); } else if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) { builder.method(method, HttpRequest.BodyPublishers.noBody()); } else { builder.method(method, HttpRequest.BodyPublishers.noBody()); } HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body()); } ApiResponse apiResponse = objectMapper.readValue(response.body(), responseType); if (apiResponse.code() != 200) { throw new ImSdkException(apiResponse.message()); } return apiResponse; } catch (IOException e) { throw new ImSdkException("Request failed: " + e.getMessage(), e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ImSdkException("Request interrupted", e); } } private URI buildUri(String path, Map query) { StringBuilder builder = new StringBuilder(baseUrl).append(path); if (query != null && !query.isEmpty()) { builder.append('?'); boolean first = true; for (Map.Entry entry : query.entrySet()) { String value = entry.getValue(); if (value == null) { continue; } if (!first) { builder.append('&'); } builder.append(encode(entry.getKey())).append('=').append(encode(value)); first = false; } } return URI.create(builder.toString()); } private Map loginQuery(String userId, String nickname, String avatar, long timestamp, String nonce) { Map query = new LinkedHashMap<>(); query.put("appId", appId); query.put("userId", userId); query.put("timestamp", String.valueOf(timestamp)); query.put("nonce", nonce); if (nickname != null && !nickname.isBlank()) { query.put("nickname", nickname); } if (avatar != null && !avatar.isBlank()) { query.put("avatar", avatar); } return query; } private Map appQuery() { Map query = new LinkedHashMap<>(); query.put("appId", appId); return query; } private Map queryWithPage(int page, int size) { Map query = appQuery(); query.put("page", String.valueOf(page)); query.put("size", String.valueOf(size)); return query; } private Map queryWithSize(String key, String value, int size) { Map query = appQuery(); query.put(key, value); query.put("size", String.valueOf(size)); return query; } private Map queryParams(String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size) { Map query = new LinkedHashMap<>(); query.put("appId", appId); if (msgType != null && !msgType.isBlank()) { query.put("msgType", msgType); } if (keyword != null && !keyword.isBlank()) { query.put("keyword", keyword); } if (startTime != null) { query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString()); } if (endTime != null) { query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString()); } query.put("page", String.valueOf(page)); query.put("size", String.valueOf(size)); return query; } private String[] flatten(Map headers) { String[] pairs = new String[headers.size() * 2]; int index = 0; for (Map.Entry entry : headers.entrySet()) { pairs[index++] = entry.getKey(); pairs[index++] = entry.getValue(); } return pairs; } private String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } private String trimTrailingSlash(String value) { return value.endsWith("/") ? value.substring(0, value.length() - 1) : value; } public static final class Builder { private String baseUrl; private String appId; private String appSecret; private Supplier bearerTokenSupplier; private Builder() {} public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } public Builder appId(String appId) { this.appId = appId; return this; } public Builder appSecret(String appSecret) { this.appSecret = appSecret; return this; } public Builder bearerTokenSupplier(Supplier bearerTokenSupplier) { this.bearerTokenSupplier = bearerTokenSupplier; return this; } public XuqmImServerSdk build() { return new XuqmImServerSdk(this); } } public record LoginResponse(String token, long expiresAt) {} public record ConversationView( String targetId, String chatType, String lastMsgContent, String lastMsgType, Long lastMsgTime, int unreadCount, boolean isMuted, boolean isPinned ) {} public record AccountView( String id, String appId, String userId, String nickname, String gender, String avatar, String status, LocalDateTime createdAt ) {} public record FriendLinkView( Long id, String appId, String userId, String friendId, Instant createdAt ) {} public record FriendRequestView( String id, String appId, String fromUserId, String toUserId, String remark, String status, LocalDateTime createdAt, LocalDateTime reviewedAt ) {} public record BlacklistView( String id, String appId, String userId, String blockedUserId, LocalDateTime createdAt ) {} public record GroupView( String id, String appId, String name, String groupType, String creatorId, String memberIds, String adminIds, String announcement, LocalDateTime createdAt ) { public List memberIdList() { return parseJsonStringList(memberIds); } public List adminIdList() { return parseJsonStringList(adminIds); } } public record GroupJoinRequestView( String id, String appId, String groupId, String requesterId, String remark, String status, LocalDateTime createdAt, LocalDateTime reviewedAt ) {} public record StatsView( long totalMessages, long totalUsers, long totalGroups, long todayMessages ) {} public record HistoryQuery( String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, Integer page, Integer size ) { public HistoryQuery() { this(null, null, null, null, null, null); } } public record PageResult( List content, long totalElements, int totalPages, int size, int number, int numberOfElements, boolean first, boolean last, boolean empty ) {} public record ImMessage( String id, String appId, String fromUserId, String toId, String chatType, String msgType, String content, String status, String mentionedUserIds, Integer groupReadCount, Long createdAt ) {} public record SendMessageRequest( String messageId, String toId, String chatType, String msgType, String content, String mentionedUserIds ) {} public record CreateGroupRequest( String name, List memberIds, String groupType ) {} public record UpdateGroupRequest( String name, String announcement ) {} public record MemberRequest(String userId) {} public record SetRoleRequest(String userId, String role) {} public record MuteMemberRequest(String userId, long minutes) {} public static final class ImSdkException extends RuntimeException { public ImSdkException(String message) { super(message); } public ImSdkException(String message, Throwable cause) { super(message, cause); } } private static List parseJsonStringList(String value) { if (value == null || value.isBlank()) { return List.of(); } String trimmed = value.trim(); if (!trimmed.startsWith("[")) { return trimmed.isEmpty() ? List.of() : List.of(trimmed.split(",")); } try { ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(trimmed, new TypeReference>() {}); } catch (IOException e) { return new ArrayList<>(List.of(trimmed)); } } }