feat(im): 添加即时通讯功能模块
- 添加了 IM API 接口定义,包含登录、消息、群组、好友等接口 - 实现了 ImSDK 核心功能,支持发送各类消息和管理会话 - 集成了 WebSocket 连接管理和自动重连机制 - 添加了本地联系人缓存并优化对话标题显示逻辑 - 实现了 HarmonyOS 平台 HTTP 客户端基础功能
这个提交包含在:
父节点
f140d8de3a
当前提交
027937db1b
@ -19,6 +19,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
private var activeWsURL: URL?
|
private var activeWsURL: URL?
|
||||||
private var activeToken: String?
|
private var activeToken: String?
|
||||||
private var activeAppId: String?
|
private var activeAppId: String?
|
||||||
|
private var activeUserId: String?
|
||||||
|
|
||||||
public init(wsURL: URL? = nil, token: String? = nil, appId: String? = nil) {
|
public init(wsURL: URL? = nil, token: String? = nil, appId: String? = nil) {
|
||||||
self.wsURLOverride = wsURL
|
self.wsURLOverride = wsURL
|
||||||
@ -27,6 +28,10 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setCurrentUserId(_ userId: String) {
|
||||||
|
activeUserId = userId
|
||||||
|
}
|
||||||
|
|
||||||
public func connect() {
|
public func connect() {
|
||||||
shouldReconnect = true
|
shouldReconnect = true
|
||||||
reconnectWorkItem?.cancel()
|
reconnectWorkItem?.cancel()
|
||||||
@ -55,7 +60,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
self.activeToken = activeToken
|
self.activeToken = activeToken
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
|
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) -> ImMessage {
|
||||||
sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content, mentionedUserIds: nil)
|
sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content, mentionedUserIds: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,12 +70,27 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
msgType: MsgType,
|
msgType: MsgType,
|
||||||
content: String,
|
content: String,
|
||||||
mentionedUserIds: String?
|
mentionedUserIds: String?
|
||||||
) {
|
) -> ImMessage {
|
||||||
|
let messageId = UUID().uuidString
|
||||||
|
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||||||
|
let message = ImMessage(
|
||||||
|
id: messageId,
|
||||||
|
appId: activeAppId ?? "",
|
||||||
|
fromUserId: activeUserId ?? "",
|
||||||
|
toId: toId,
|
||||||
|
chatType: chatType,
|
||||||
|
msgType: msgType,
|
||||||
|
content: content,
|
||||||
|
status: .sending,
|
||||||
|
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
|
||||||
|
groupReadCount: nil,
|
||||||
|
createdAt: now
|
||||||
|
)
|
||||||
guard let activeAppId else {
|
guard let activeAppId else {
|
||||||
delegate?.imClientDidError("IM appId not configured")
|
delegate?.imClientDidError("IM appId not configured")
|
||||||
return
|
return message.failedCopy()
|
||||||
}
|
}
|
||||||
sendFrame(
|
let sent = sendFrame(
|
||||||
command: "SEND",
|
command: "SEND",
|
||||||
headers: [
|
headers: [
|
||||||
"destination": "/app/chat.send",
|
"destination": "/app/chat.send",
|
||||||
@ -78,6 +98,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
],
|
],
|
||||||
body: encodeJSONString([
|
body: encodeJSONString([
|
||||||
"appId": activeAppId,
|
"appId": activeAppId,
|
||||||
|
"messageId": messageId,
|
||||||
"toId": toId,
|
"toId": toId,
|
||||||
"chatType": chatType.rawValue,
|
"chatType": chatType.rawValue,
|
||||||
"msgType": msgType.rawValue,
|
"msgType": msgType.rawValue,
|
||||||
@ -85,6 +106,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
"mentionedUserIds": mentionedUserIds ?? "",
|
"mentionedUserIds": mentionedUserIds ?? "",
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
return sent ? message : message.failedCopy()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func revoke(messageId: String) {
|
public func revoke(messageId: String) {
|
||||||
@ -202,13 +224,14 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
DispatchQueue.global().asyncAfter(deadline: .now() + reconnectDelay, execute: workItem)
|
DispatchQueue.global().asyncAfter(deadline: .now() + reconnectDelay, execute: workItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendFrame(command: String, headers: [String: String] = [:], body: String = "") {
|
private func sendFrame(command: String, headers: [String: String] = [:], body: String = "") -> Bool {
|
||||||
guard let webSocketTask else { return }
|
guard let webSocketTask, webSocketTask.state == .running else { return false }
|
||||||
let headerLines = headers.map { "\($0.key):\($0.value)" }.joined(separator: "\n")
|
let headerLines = headers.map { "\($0.key):\($0.value)" }.joined(separator: "\n")
|
||||||
let frame = headerLines.isEmpty
|
let frame = headerLines.isEmpty
|
||||||
? "\(command)\n\n\(body)\u{0000}"
|
? "\(command)\n\n\(body)\u{0000}"
|
||||||
: "\(command)\n\(headerLines)\n\n\(body)\u{0000}"
|
: "\(command)\n\(headerLines)\n\n\(body)\u{0000}"
|
||||||
webSocketTask.send(.string(frame)) { _ in }
|
webSocketTask.send(.string(frame)) { _ in }
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func encodeJSONString(_ object: [String: String]) -> String {
|
private func encodeJSONString(_ object: [String: String]) -> String {
|
||||||
@ -262,6 +285,24 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ImMessage {
|
||||||
|
func failedCopy() -> ImMessage {
|
||||||
|
ImMessage(
|
||||||
|
id: id,
|
||||||
|
appId: appId,
|
||||||
|
fromUserId: fromUserId,
|
||||||
|
toId: toId,
|
||||||
|
chatType: chatType,
|
||||||
|
msgType: msgType,
|
||||||
|
content: content,
|
||||||
|
status: .failed,
|
||||||
|
mentionedUserIds: mentionedUserIds,
|
||||||
|
groupReadCount: groupReadCount,
|
||||||
|
createdAt: createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct StompFrame {
|
private struct StompFrame {
|
||||||
let command: String
|
let command: String
|
||||||
let headers: [String: String]
|
let headers: [String: String]
|
||||||
|
|||||||
@ -6,6 +6,7 @@ public final class ImSDK {
|
|||||||
public static let shared = ImSDK()
|
public static let shared = ImSDK()
|
||||||
private var client: ImClient?
|
private var client: ImClient?
|
||||||
private weak var delegate: ImEventDelegate?
|
private weak var delegate: ImEventDelegate?
|
||||||
|
private var currentUserId: String?
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
@ -24,9 +25,11 @@ public final class ImSDK {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
queryItems: items
|
queryItems: items
|
||||||
)
|
)
|
||||||
|
currentUserId = userId
|
||||||
XuqmSDK.shared.tokenStore?.save(res.token)
|
XuqmSDK.shared.tokenStore?.save(res.token)
|
||||||
client?.disconnect()
|
client?.disconnect()
|
||||||
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
|
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
|
||||||
|
client?.setCurrentUserId(userId)
|
||||||
client?.delegate = delegate
|
client?.delegate = delegate
|
||||||
client?.connect()
|
client?.connect()
|
||||||
}
|
}
|
||||||
@ -36,11 +39,12 @@ public final class ImSDK {
|
|||||||
client?.delegate = delegate
|
client?.delegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
|
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) -> ImMessage {
|
||||||
client?.sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content)
|
client?.sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content)
|
||||||
|
?? fallbackFailedMessage(toId: toId, chatType: chatType, msgType: msgType, content: content, mentionedUserIds: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendTextMessage(toId: String, chatType: ChatType, content: String) {
|
public func sendTextMessage(toId: String, chatType: ChatType, content: String) -> ImMessage {
|
||||||
sendMessage(toId: toId, chatType: chatType, msgType: .text, content: content)
|
sendMessage(toId: toId, chatType: chatType, msgType: .text, content: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +56,7 @@ public final class ImSDK {
|
|||||||
width: Int? = nil,
|
width: Int? = nil,
|
||||||
height: Int? = nil,
|
height: Int? = nil,
|
||||||
size: Int64? = nil
|
size: Int64? = nil
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -74,7 +78,7 @@ public final class ImSDK {
|
|||||||
thumbnailUrl: String? = nil,
|
thumbnailUrl: String? = nil,
|
||||||
duration: Int64? = nil,
|
duration: Int64? = nil,
|
||||||
size: Int64? = nil
|
size: Int64? = nil
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -94,7 +98,7 @@ public final class ImSDK {
|
|||||||
url: String,
|
url: String,
|
||||||
duration: Int64? = nil,
|
duration: Int64? = nil,
|
||||||
size: Int64? = nil
|
size: Int64? = nil
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -113,7 +117,7 @@ public final class ImSDK {
|
|||||||
url: String,
|
url: String,
|
||||||
name: String,
|
name: String,
|
||||||
size: Int64
|
size: Int64
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -133,7 +137,7 @@ public final class ImSDK {
|
|||||||
lng: Double,
|
lng: Double,
|
||||||
title: String? = nil,
|
title: String? = nil,
|
||||||
address: String? = nil
|
address: String? = nil
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -147,7 +151,7 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendCustomMessage(toId: String, chatType: ChatType, data: String) {
|
public func sendCustomMessage(toId: String, chatType: ChatType, data: String) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -156,7 +160,7 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendRichTextMessage(toId: String, chatType: ChatType, html: String) {
|
public func sendRichTextMessage(toId: String, chatType: ChatType, html: String) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -165,7 +169,7 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendNotifyMessage(toId: String, chatType: ChatType, title: String, content: String) {
|
public func sendNotifyMessage(toId: String, chatType: ChatType, title: String, content: String) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -180,7 +184,7 @@ public final class ImSDK {
|
|||||||
quotedMsgId: String,
|
quotedMsgId: String,
|
||||||
quotedContent: String,
|
quotedContent: String,
|
||||||
text: String
|
text: String
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -198,7 +202,7 @@ public final class ImSDK {
|
|||||||
chatType: ChatType,
|
chatType: ChatType,
|
||||||
title: String,
|
title: String,
|
||||||
msgList: [String]
|
msgList: [String]
|
||||||
) {
|
) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -210,7 +214,7 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendForwardMessage(toId: String, chatType: ChatType, originalSender: String, originalContent: String) {
|
public func sendForwardMessage(toId: String, chatType: ChatType, originalSender: String, originalContent: String) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -222,7 +226,7 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendCallAudioMessage(toId: String, chatType: ChatType, action: String) {
|
public func sendCallAudioMessage(toId: String, chatType: ChatType, action: String) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -231,7 +235,7 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendCallVideoMessage(toId: String, chatType: ChatType, action: String) {
|
public func sendCallVideoMessage(toId: String, chatType: ChatType, action: String) -> ImMessage {
|
||||||
sendMessage(
|
sendMessage(
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
@ -252,6 +256,28 @@ public final class ImSDK {
|
|||||||
client?.unsubscribeGroup(groupId)
|
client?.unsubscribeGroup(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fallbackFailedMessage(
|
||||||
|
toId: String,
|
||||||
|
chatType: ChatType,
|
||||||
|
msgType: MsgType,
|
||||||
|
content: String,
|
||||||
|
mentionedUserIds: String?
|
||||||
|
) -> ImMessage {
|
||||||
|
ImMessage(
|
||||||
|
id: UUID().uuidString,
|
||||||
|
appId: XuqmSDK.shared.requireConfig().appId,
|
||||||
|
fromUserId: currentUserId ?? "",
|
||||||
|
toId: toId,
|
||||||
|
chatType: chatType,
|
||||||
|
msgType: msgType,
|
||||||
|
content: content,
|
||||||
|
status: .failed,
|
||||||
|
mentionedUserIds: mentionedUserIds,
|
||||||
|
groupReadCount: nil,
|
||||||
|
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public func fetchHistory(
|
public func fetchHistory(
|
||||||
toId: String,
|
toId: String,
|
||||||
page: Int = 0,
|
page: Int = 0,
|
||||||
@ -521,6 +547,32 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getProfile(userId: String) async throws -> UserProfile {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
return try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/accounts/\(userId)",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateProfile(
|
||||||
|
userId: String,
|
||||||
|
nickname: String? = nil,
|
||||||
|
avatar: String? = nil,
|
||||||
|
gender: String? = nil
|
||||||
|
) async throws -> UserProfile {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
var items = [URLQueryItem(name: "appId", value: config.appId)]
|
||||||
|
if let nickname { items.append(URLQueryItem(name: "nickname", value: nickname)) }
|
||||||
|
if let avatar { items.append(URLQueryItem(name: "avatar", value: avatar)) }
|
||||||
|
if let gender { items.append(URLQueryItem(name: "gender", value: gender)) }
|
||||||
|
return try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/accounts/\(userId)",
|
||||||
|
method: "PUT",
|
||||||
|
queryItems: items
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public func listConversations() async throws -> [ConversationData] {
|
public func listConversations() async throws -> [ConversationData] {
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
return try await ApiClient.shared.request(
|
return try await ApiClient.shared.request(
|
||||||
|
|||||||
@ -107,7 +107,19 @@ public struct BlacklistEntry: Codable, Sendable {
|
|||||||
public let createdAt: Int64
|
public let createdAt: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct UserProfile: Codable, Sendable {
|
||||||
|
public let id: String?
|
||||||
|
public let appId: String?
|
||||||
|
public let userId: String
|
||||||
|
public let nickname: String?
|
||||||
|
public let avatar: String?
|
||||||
|
public let gender: String?
|
||||||
|
public let status: String?
|
||||||
|
public let createdAt: Int64?
|
||||||
|
}
|
||||||
|
|
||||||
public struct ImSendMessageRequest: Encodable, Sendable {
|
public struct ImSendMessageRequest: Encodable, Sendable {
|
||||||
|
public let messageId: String
|
||||||
public let toId: String
|
public let toId: String
|
||||||
public let chatType: ChatType
|
public let chatType: ChatType
|
||||||
public let msgType: MsgType
|
public let msgType: MsgType
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户