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 activeToken: String?
|
||||
private var activeAppId: String?
|
||||
private var activeUserId: String?
|
||||
|
||||
public init(wsURL: URL? = nil, token: String? = nil, appId: String? = nil) {
|
||||
self.wsURLOverride = wsURL
|
||||
@ -27,6 +28,10 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func setCurrentUserId(_ userId: String) {
|
||||
activeUserId = userId
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
shouldReconnect = true
|
||||
reconnectWorkItem?.cancel()
|
||||
@ -55,7 +60,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
||||
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)
|
||||
}
|
||||
|
||||
@ -65,12 +70,27 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
||||
msgType: MsgType,
|
||||
content: 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 {
|
||||
delegate?.imClientDidError("IM appId not configured")
|
||||
return
|
||||
return message.failedCopy()
|
||||
}
|
||||
sendFrame(
|
||||
let sent = sendFrame(
|
||||
command: "SEND",
|
||||
headers: [
|
||||
"destination": "/app/chat.send",
|
||||
@ -78,6 +98,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
||||
],
|
||||
body: encodeJSONString([
|
||||
"appId": activeAppId,
|
||||
"messageId": messageId,
|
||||
"toId": toId,
|
||||
"chatType": chatType.rawValue,
|
||||
"msgType": msgType.rawValue,
|
||||
@ -85,6 +106,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
||||
"mentionedUserIds": mentionedUserIds ?? "",
|
||||
]),
|
||||
)
|
||||
return sent ? message : message.failedCopy()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private func sendFrame(command: String, headers: [String: String] = [:], body: String = "") {
|
||||
guard let webSocketTask else { return }
|
||||
private func sendFrame(command: String, headers: [String: String] = [:], body: String = "") -> Bool {
|
||||
guard let webSocketTask, webSocketTask.state == .running else { return false }
|
||||
let headerLines = headers.map { "\($0.key):\($0.value)" }.joined(separator: "\n")
|
||||
let frame = headerLines.isEmpty
|
||||
? "\(command)\n\n\(body)\u{0000}"
|
||||
: "\(command)\n\(headerLines)\n\n\(body)\u{0000}"
|
||||
webSocketTask.send(.string(frame)) { _ in }
|
||||
return true
|
||||
}
|
||||
|
||||
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 {
|
||||
let command: String
|
||||
let headers: [String: String]
|
||||
|
||||
@ -6,6 +6,7 @@ public final class ImSDK {
|
||||
public static let shared = ImSDK()
|
||||
private var client: ImClient?
|
||||
private weak var delegate: ImEventDelegate?
|
||||
private var currentUserId: String?
|
||||
|
||||
private init() {}
|
||||
|
||||
@ -24,9 +25,11 @@ public final class ImSDK {
|
||||
method: "POST",
|
||||
queryItems: items
|
||||
)
|
||||
currentUserId = userId
|
||||
XuqmSDK.shared.tokenStore?.save(res.token)
|
||||
client?.disconnect()
|
||||
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
|
||||
client?.setCurrentUserId(userId)
|
||||
client?.delegate = delegate
|
||||
client?.connect()
|
||||
}
|
||||
@ -36,11 +39,12 @@ public final class ImSDK {
|
||||
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)
|
||||
?? 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)
|
||||
}
|
||||
|
||||
@ -52,7 +56,7 @@ public final class ImSDK {
|
||||
width: Int? = nil,
|
||||
height: Int? = nil,
|
||||
size: Int64? = nil
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -74,7 +78,7 @@ public final class ImSDK {
|
||||
thumbnailUrl: String? = nil,
|
||||
duration: Int64? = nil,
|
||||
size: Int64? = nil
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -94,7 +98,7 @@ public final class ImSDK {
|
||||
url: String,
|
||||
duration: Int64? = nil,
|
||||
size: Int64? = nil
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -113,7 +117,7 @@ public final class ImSDK {
|
||||
url: String,
|
||||
name: String,
|
||||
size: Int64
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -133,7 +137,7 @@ public final class ImSDK {
|
||||
lng: Double,
|
||||
title: String? = nil,
|
||||
address: String? = nil
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
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(
|
||||
toId: toId,
|
||||
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(
|
||||
toId: toId,
|
||||
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(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -180,7 +184,7 @@ public final class ImSDK {
|
||||
quotedMsgId: String,
|
||||
quotedContent: String,
|
||||
text: String
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -198,7 +202,7 @@ public final class ImSDK {
|
||||
chatType: ChatType,
|
||||
title: String,
|
||||
msgList: [String]
|
||||
) {
|
||||
) -> ImMessage {
|
||||
sendMessage(
|
||||
toId: toId,
|
||||
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(
|
||||
toId: toId,
|
||||
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(
|
||||
toId: toId,
|
||||
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(
|
||||
toId: toId,
|
||||
chatType: chatType,
|
||||
@ -252,6 +256,28 @@ public final class ImSDK {
|
||||
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(
|
||||
toId: String,
|
||||
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] {
|
||||
let config = XuqmSDK.shared.requireConfig()
|
||||
return try await ApiClient.shared.request(
|
||||
|
||||
@ -107,7 +107,19 @@ public struct BlacklistEntry: Codable, Sendable {
|
||||
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 let messageId: String
|
||||
public let toId: String
|
||||
public let chatType: ChatType
|
||||
public let msgType: MsgType
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户