diff --git a/docs/PLATFORM_OVERVIEW.md b/docs/PLATFORM_OVERVIEW.md index 6cda91b..6d66226 100644 --- a/docs/PLATFORM_OVERVIEW.md +++ b/docs/PLATFORM_OVERVIEW.md @@ -83,7 +83,12 @@ | 平台 | Registry | 命令 | |------|----------|------| | npm (RN SDK / Vue3 SDK) | https://nexus.xuqinmin.com/repository/npm-hosted/ | `npm publish` | -| Android Maven | https://nexus.xuqinmin.com/repository/android-hosted/ | `./gradlew publish` | +| npm download | https://nexus.xuqinmin.com/repository/npm/ | client consumption | +| Android Maven upload | https://nexus.xuqinmin.com/repository/android-hosted/ | `./gradlew publish` | +| Android Maven download | https://nexus.xuqinmin.com/repository/android/ | client consumption | +| Java Maven upload (release) | https://nexus.xuqinmin.com/repository/maven-releases/ | `mvn deploy` | +| Java Maven upload (snapshot) | https://nexus.xuqinmin.com/repository/maven-snapshots/ | `mvn deploy` | +| Java Maven download | https://nexus.xuqinmin.com/repository/maven-public/ | client consumption | | iOS SPM | Git Tag | `git tag x.y.z && git push origin x.y.z` | | iOS CocoaPods | https://xuqinmin.com/xuqinmin12/xuqm-specs | `pod repo push xuqm-specs XuqmSDK.podspec` | | HarmonyOS | ohpm (OpenHarmony Package Manager) | `ohpm publish` | diff --git a/im-sdk/pom.xml b/im-sdk/pom.xml new file mode 100644 index 0000000..b48c65e --- /dev/null +++ b/im-sdk/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + com.xuqm + xuqmgroup-server-parent + 0.1.0-SNAPSHOT + ../pom.xml + + + im-sdk + im-sdk + Java SDK for XuqmGroup IM service and REST APIs + + + + com.xuqm + common + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + xuqm-maven-releases + https://nexus.xuqinmin.com/repository/maven-releases/ + + + xuqm-maven-snapshots + https://nexus.xuqinmin.com/repository/maven-snapshots/ + + + diff --git a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java new file mode 100644 index 0000000..bd84561 --- /dev/null +++ b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java @@ -0,0 +1,1009 @@ +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)); + } + } +} diff --git a/im-service/src/main/java/com/xuqm/im/controller/AccountController.java b/im-service/src/main/java/com/xuqm/im/controller/AccountController.java new file mode 100644 index 0000000..e05b5ed --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/controller/AccountController.java @@ -0,0 +1,50 @@ +package com.xuqm.im.controller; + +import com.xuqm.common.exception.BusinessException; +import com.xuqm.common.model.ApiResponse; +import com.xuqm.im.entity.ImAccountEntity; +import com.xuqm.im.service.ImAccountService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +@RestController +@RequestMapping("/api/im/accounts") +public class AccountController { + + private final ImAccountService accountService; + + public AccountController(ImAccountService accountService) { + this.accountService = accountService; + } + + @GetMapping("/{userId}") + public ResponseEntity> get( + @AuthenticationPrincipal String currentUserId, + @RequestParam String appId, + @PathVariable String userId) { + return ResponseEntity.ok(ApiResponse.success(accountService.getAccount(appId, userId))); + } + + @PutMapping("/{userId}") + public ResponseEntity> update( + @AuthenticationPrincipal String currentUserId, + @RequestParam String appId, + @PathVariable String userId, + @RequestParam(required = false) String nickname, + @RequestParam(required = false) String avatar, + @RequestParam(required = false) ImAccountEntity.Gender gender) { + if (!Objects.equals(currentUserId, userId)) { + throw new BusinessException(403, "Only the account owner can update profile"); + } + return ResponseEntity.ok(ApiResponse.success( + accountService.updateAccount(appId, userId, nickname, avatar, gender))); + } +} diff --git a/pom.xml b/pom.xml index 3c2570b..fd616b8 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ common tenant-service im-service + im-sdk push-service update-service demo-service