From 8fee092c44a1a7b9dd5f2516b8f5e260e4d15188 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Sat, 2 May 2026 22:57:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(android-sdk):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=9A=84IM=E5=AE=A2=E6=88=B7=E7=AB=AFSDK?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能 - 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力 - 实现了群组管理功能,包括创建、成员管理、权限设置等操作 - 添加了好友关系链管理,支持添加、删除、分组等操作 - 实现了会话管理功能,包括置顶、免打扰、已读状态等 - 添加了黑名单、资料管理、搜索等辅助功能 - 补齐了批量操作接口,提升客户端操作效率 - 实现了WebSocket连接管理和事件监听机制 - 添加了离线消息同步和状态管理功能 --- Sources/XuqmSDK/IM/ImClient.swift | 20 ++++++ Sources/XuqmSDK/IM/ImSDK.swift | 108 +++++++++++++++++++++++++++++ Sources/XuqmSDK/IM/ImTypes.swift | 17 +++++ Sources/XuqmSDK/Push/PushSDK.swift | 49 ++++++++++++- 4 files changed, 193 insertions(+), 1 deletion(-) diff --git a/Sources/XuqmSDK/IM/ImClient.swift b/Sources/XuqmSDK/IM/ImClient.swift index 956f414..1ed0555 100644 --- a/Sources/XuqmSDK/IM/ImClient.swift +++ b/Sources/XuqmSDK/IM/ImClient.swift @@ -128,6 +128,25 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S ) } + public func sync() { + guard let activeAppId else { + delegate?.imClientDidError("IM appId not configured") + return + } + sendFrame( + command: "SEND", + headers: [ + "destination": "/app/chat.sync", + "content-type": "application/json", + ], + body: encodeJSONString(["appId": activeAppId]), + ) + } + + private func sendSync() { + sync() + } + public func subscribeGroup(_ groupId: String) { let subscriptionKey = "group-\(groupId)" let isNew = groupSubscriptions.insert(groupId).inserted @@ -194,6 +213,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S "id": "group-\(groupId)", ]) } + sendSync() delegate?.imClientDidConnect() case "MESSAGE": guard let messageData = frame.body.data(using: .utf8), diff --git a/Sources/XuqmSDK/IM/ImSDK.swift b/Sources/XuqmSDK/IM/ImSDK.swift index 473471f..a1fe736 100644 --- a/Sources/XuqmSDK/IM/ImSDK.swift +++ b/Sources/XuqmSDK/IM/ImSDK.swift @@ -908,6 +908,24 @@ public final class ImSDK { return conversations.reduce(0) { $0 + $1.unreadCount } } + public func offlineMessageCount() async throws -> Int { + let config = XuqmSDK.shared.requireConfig() + let result: [String: Int] = try await ApiClient.shared.request( + path: "/api/im/messages/offline/count", + queryItems: [URLQueryItem(name: "appId", value: config.appId)] + ) + return result["count"] ?? 0 + } + + public func syncOfflineMessages() async throws -> [ImMessage] { + let config = XuqmSDK.shared.requireConfig() + return try await ApiClient.shared.request( + path: "/api/im/messages/offline", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)] + ) + } + public func adminGroupReadReceipts(groupId: String, messageIds: [String]) async throws -> [GroupReadReceiptSummary] { let config = XuqmSDK.shared.requireConfig() return try await ApiClient.shared.request( @@ -918,6 +936,96 @@ public final class ImSDK { ) } + public func batchAddFriends(friendIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/friends/batch", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchFriendIdsRequest(friendIds: friendIds) + ) + } + + public func batchRemoveFriends(friendIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/friends/batch/remove", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchFriendIdsRequest(friendIds: friendIds) + ) + } + + public func batchAcceptFriendRequests(requestIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/friend-requests/batch/accept", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchRequestIdsRequest(requestIds: requestIds) + ) + } + + public func batchRejectFriendRequests(requestIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/friend-requests/batch/reject", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchRequestIdsRequest(requestIds: requestIds) + ) + } + + public func batchAddGroupMembers(groupId: String, userIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/groups/\(groupId)/members/batch", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchUserIdsRequest(userIds: userIds) + ) + } + + public func batchRemoveGroupMembers(groupId: String, userIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/groups/\(groupId)/members/batch/remove", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchUserIdsRequest(userIds: userIds) + ) + } + + public func batchAcceptGroupJoinRequests(groupId: String, requestIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/groups/\(groupId)/join-requests/batch/accept", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchRequestIdsRequest(requestIds: requestIds) + ) + } + + public func batchRejectGroupJoinRequests(groupId: String, requestIds: [String]) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/groups/\(groupId)/join-requests/batch/reject", + method: "POST", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: BatchRequestIdsRequest(requestIds: requestIds) + ) + } + + public func modifyGroupMemberInfo(groupId: String, userId: String, nickname: String? = nil, role: String? = nil) async throws { + let config = XuqmSDK.shared.requireConfig() + let _: EmptyResponse = try await ApiClient.shared.request( + path: "/api/im/groups/\(groupId)/members/\(userId)/info", + method: "PUT", + queryItems: [URLQueryItem(name: "appId", value: config.appId)], + body: ModifyMemberInfoRequest(nickname: nickname, role: role) + ) + } + public func disconnect() { client?.disconnect() client = nil diff --git a/Sources/XuqmSDK/IM/ImTypes.swift b/Sources/XuqmSDK/IM/ImTypes.swift index 372e6d4..066863d 100644 --- a/Sources/XuqmSDK/IM/ImTypes.swift +++ b/Sources/XuqmSDK/IM/ImTypes.swift @@ -224,6 +224,23 @@ public struct ImGroupReadReceiptRequest: Encodable, Sendable { public let messageIds: [String] } +public struct BatchFriendIdsRequest: Encodable, Sendable { + public let friendIds: [String] +} + +public struct BatchRequestIdsRequest: Encodable, Sendable { + public let requestIds: [String] +} + +public struct BatchUserIdsRequest: Encodable, Sendable { + public let userIds: [String] +} + +public struct ModifyMemberInfoRequest: Encodable, Sendable { + public let nickname: String? + public let role: String? +} + public enum ImAttributeValue: Codable, Sendable { case string(String) case int(Int) diff --git a/Sources/XuqmSDK/Push/PushSDK.swift b/Sources/XuqmSDK/Push/PushSDK.swift index 43f6235..13d4a67 100644 --- a/Sources/XuqmSDK/Push/PushSDK.swift +++ b/Sources/XuqmSDK/Push/PushSDK.swift @@ -9,10 +9,23 @@ public enum PushVendor: String { case fcm = "FCM" } +public struct PushMessage: Sendable { + public let title: String? + public let body: String? + public let payload: [String: Any] + public let rawUserInfo: [AnyHashable: Any] +} + +public protocol PushMessageDelegate: AnyObject, Sendable { + func pushSDK(_ sdk: PushSDK, didReceiveMessage message: PushMessage) + func pushSDK(_ sdk: PushSDK, didTapNotification message: PushMessage) +} + @MainActor public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { public static let shared = PushSDK() + public weak var delegate: PushMessageDelegate? private override init() { super.init() } @@ -65,7 +78,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { let config = XuqmSDK.shared.requireConfig() let _: EmptyResponse = try await ApiClient.shared.request( path: "/api/push/unregister", - method: "POST", + method: "DELETE", queryItems: [ URLQueryItem(name: "appId", value: config.appId), URLQueryItem(name: "userId", value: userId), @@ -79,6 +92,10 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { + let message = parseMessage(notification.request.content) + if let message = message { + delegate?.pushSDK(self, didReceiveMessage: message) + } completionHandler([.banner, .sound, .badge]) } @@ -87,6 +104,36 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { + let message = parseMessage(response.notification.request.content) + if let message = message { + delegate?.pushSDK(self, didTapNotification: message) + } completionHandler() } + + private func parseMessage(_ content: UNNotificationContent) -> PushMessage? { + let userInfo = content.userInfo + let aps = userInfo["aps"] as? [String: Any] + let alert = aps?["alert"] as? [String: Any] + let title = content.title.isEmpty ? (alert?["title"] as? String) : content.title + let body = content.body.isEmpty ? (alert?["body"] as? String) : content.body + return PushMessage( + title: title, + body: body, + payload: userInfo.compactMapKeys { $0 as? String }, + rawUserInfo: userInfo + ) + } +} + +private extension Dictionary { + func compactMapKeys(_ transform: (Key) -> T?) -> [T: Value] { + var result: [T: Value] = [:] + for (key, value) in self { + if let newKey = transform(key) { + result[newKey] = value + } + } + return result + } }