XuqmGroup-iOSSDK/Sources/XuqmSDK/IM/ImSDK.swift

707 行
24 KiB
Swift

2026-04-21 22:07:29 +08:00
import Foundation
@MainActor
public final class ImSDK {
public static let shared = ImSDK()
private var client: ImClient?
private weak var delegate: ImEventDelegate?
private var currentUserId: String?
2026-04-21 22:07:29 +08:00
private init() {}
public func login(userId: String, nickname: String? = nil, avatar: String? = nil) async throws {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "userId", value: userId),
]
if let nickname { items.append(URLQueryItem(name: "nickname", value: nickname)) }
if let avatar { items.append(URLQueryItem(name: "avatar", value: avatar)) }
2026-04-21 22:07:29 +08:00
let res: ImLoginResponse = try await ApiClient.shared.request(
path: "/api/im/auth/login",
method: "POST",
queryItems: items
2026-04-21 22:07:29 +08:00
)
currentUserId = userId
2026-04-21 22:07:29 +08:00
XuqmSDK.shared.tokenStore?.save(res.token)
client?.disconnect()
2026-04-21 22:07:29 +08:00
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
client?.setCurrentUserId(userId)
client?.delegate = delegate
2026-04-21 22:07:29 +08:00
client?.connect()
}
public func setDelegate(_ delegate: ImEventDelegate) {
self.delegate = delegate
2026-04-21 22:07:29 +08:00
client?.delegate = delegate
}
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) -> ImMessage {
2026-04-21 22:07:29 +08:00
client?.sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content)
?? fallbackFailedMessage(toId: toId, chatType: chatType, msgType: msgType, content: content, mentionedUserIds: nil)
2026-04-21 22:07:29 +08:00
}
public func sendTextMessage(toId: String, chatType: ChatType, content: String) -> ImMessage {
sendMessage(toId: toId, chatType: chatType, msgType: .text, content: content)
}
public func sendImageMessage(
toId: String,
chatType: ChatType,
url: String,
thumbnailUrl: String? = nil,
width: Int? = nil,
height: Int? = nil,
size: Int64? = nil
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .image,
content: jsonString(from: [
"url": url,
"thumbnailUrl": thumbnailUrl as Any,
"width": width as Any,
"height": height as Any,
"size": size as Any,
])
)
}
public func sendVideoMessage(
toId: String,
chatType: ChatType,
url: String,
thumbnailUrl: String? = nil,
duration: Int64? = nil,
size: Int64? = nil
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .video,
content: jsonString(from: [
"url": url,
"thumbnailUrl": thumbnailUrl as Any,
"duration": duration as Any,
"size": size as Any,
])
)
}
public func sendAudioMessage(
toId: String,
chatType: ChatType,
url: String,
duration: Int64? = nil,
size: Int64? = nil
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .audio,
content: jsonString(from: [
"url": url,
"duration": duration as Any,
"size": size as Any,
])
)
}
public func sendFileMessage(
toId: String,
chatType: ChatType,
url: String,
name: String,
size: Int64
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .file,
content: jsonString(from: [
"url": url,
"name": name,
"size": size,
])
)
}
public func sendLocationMessage(
toId: String,
chatType: ChatType,
lat: Double,
lng: Double,
title: String? = nil,
address: String? = nil
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .location,
content: jsonString(from: [
"lat": lat,
"lng": lng,
"title": title as Any,
"address": address as Any,
])
)
}
public func sendCustomMessage(toId: String, chatType: ChatType, data: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .custom,
content: jsonString(from: ["data": data])
)
}
public func sendRichTextMessage(toId: String, chatType: ChatType, html: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .richText,
content: jsonString(from: ["html": html])
)
}
public func sendNotifyMessage(toId: String, chatType: ChatType, title: String, content: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .notify,
content: jsonString(from: ["title": title, "content": content])
)
}
public func sendQuoteMessage(
toId: String,
chatType: ChatType,
quotedMsgId: String,
quotedContent: String,
text: String
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .quote,
content: jsonString(from: [
"quotedMsgId": quotedMsgId,
"quotedContent": quotedContent,
"text": text,
])
)
}
public func sendMergeMessage(
toId: String,
chatType: ChatType,
title: String,
msgList: [String]
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .merge,
content: jsonString(from: [
"title": title,
"msgList": msgList,
])
)
}
public func sendForwardMessage(toId: String, chatType: ChatType, originalSender: String, originalContent: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .forward,
content: jsonString(from: [
"originalSender": originalSender,
"originalContent": originalContent,
])
)
}
public func sendCallAudioMessage(toId: String, chatType: ChatType, action: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .callAudio,
content: jsonString(from: ["action": action])
)
}
public func sendCallVideoMessage(toId: String, chatType: ChatType, action: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
msgType: .callVideo,
content: jsonString(from: ["action": action])
)
}
public func revokeMessage(messageId: String) {
client?.revoke(messageId: messageId)
}
public func subscribeGroup(_ groupId: String) {
client?.subscribeGroup(groupId)
}
public func unsubscribeGroup(_ groupId: String) {
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,
size: Int = 20,
msgType: MsgType? = nil,
keyword: String? = nil,
startTime: Date? = nil,
endTime: Date? = nil
) async throws -> [ImMessage] {
try await fetchHistoryInternal(
toId: toId,
page: page,
size: size,
msgType: msgType,
keyword: keyword,
startTime: startTime,
endTime: endTime
)
}
public func fetchGroupHistory(
groupId: String,
page: Int = 0,
size: Int = 20,
msgType: MsgType? = nil,
keyword: String? = nil,
startTime: Date? = nil,
endTime: Date? = nil
) async throws -> [ImMessage] {
try await fetchHistoryInternal(
groupId: groupId,
page: page,
size: size,
msgType: msgType,
keyword: keyword,
startTime: startTime,
endTime: endTime,
isGroup: true
)
}
public func listGroups() async throws -> [ImGroup] {
let config = XuqmSDK.shared.requireConfig()
let groups: [ImGroup] = try await ApiClient.shared.request(
path: "/api/im/groups",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
return groups
}
public func listPublicGroups(keyword: String? = nil) async throws -> [ImGroup] {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
if let keyword { items.append(URLQueryItem(name: "keyword", value: keyword)) }
let groups: [ImGroup] = try await ApiClient.shared.request(
path: "/api/im/groups/public",
queryItems: items
)
return groups
}
public func createGroup(name: String, memberIds: [String], groupType: String = "WORK") async throws -> ImGroup {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
body: ImCreateGroupRequest(name: name, memberIds: memberIds, groupType: groupType)
)
}
public func getGroupInfo(groupId: String) async throws -> ImGroup {
try await ApiClient.shared.request(path: "/api/im/groups/\(groupId)")
}
public func updateGroupInfo(groupId: String, name: String? = nil, announcement: String? = nil) async throws -> ImGroup {
try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)",
method: "PUT",
body: ImUpdateGroupRequest(name: name, announcement: announcement)
)
}
public func addGroupMember(groupId: String, userId: String) async throws {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members",
method: "POST",
body: ImMemberRequest(userId: userId)
)
}
public func removeGroupMember(groupId: String, targetUserId: String) async throws {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/\(targetUserId)",
method: "DELETE"
)
}
public func leaveGroup(groupId: String) async throws {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/me",
method: "DELETE"
)
}
public func setGroupRole(groupId: String, userId: String, role: String) async throws -> ImGroup {
try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/roles",
method: "POST",
body: ImSetRoleRequest(userId: userId, role: role)
)
}
public func muteGroupMember(groupId: String, userId: String, minutes: Int64) async throws -> ImGroup {
try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/mute",
method: "POST",
body: ImMuteMemberRequest(userId: userId, minutes: minutes)
)
}
public func dismissGroup(groupId: String) async throws {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)",
method: "DELETE"
)
}
public func sendGroupJoinRequest(groupId: String, remark: String? = nil) async throws -> GroupJoinRequest {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
if let remark { items.append(URLQueryItem(name: "remark", value: remark)) }
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests",
method: "POST",
queryItems: items
)
}
public func listGroupJoinRequests(groupId: String) async throws -> [GroupJoinRequest] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func acceptGroupJoinRequest(groupId: String, requestId: String) async throws -> GroupJoinRequest {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests/\(requestId)/accept",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func rejectGroupJoinRequest(groupId: String, requestId: String) async throws -> GroupJoinRequest {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests/\(requestId)/reject",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func listFriends() async throws -> [String] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friends",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func addFriend(friendId: String) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends",
method: "POST",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "friendId", value: friendId),
]
)
}
public func removeFriend(friendId: String) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends/\(friendId)",
method: "DELETE",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func listFriendRequests(direction: String = "incoming") async throws -> [FriendRequest] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friend-requests",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "direction", value: direction),
]
)
}
public func sendFriendRequest(toUserId: String, remark: String? = nil) async throws -> FriendRequest {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "toUserId", value: toUserId),
]
if let remark { items.append(URLQueryItem(name: "remark", value: remark)) }
return try await ApiClient.shared.request(
path: "/api/im/friend-requests",
method: "POST",
queryItems: items
)
}
public func acceptFriendRequest(requestId: String) async throws -> FriendRequest {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friend-requests/\(requestId)/accept",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func rejectFriendRequest(requestId: String) async throws -> FriendRequest {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friend-requests/\(requestId)/reject",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func listBlacklist() async throws -> [BlacklistEntry] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/blacklist",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func addToBlacklist(blockedUserId: String) async throws -> BlacklistEntry {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/blacklist",
method: "POST",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "blockedUserId", value: blockedUserId),
]
)
}
public func removeFromBlacklist(blockedUserId: String) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/blacklist",
method: "DELETE",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "blockedUserId", value: blockedUserId),
]
)
}
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(
path: "/api/im/conversations",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func markRead(targetId: String, chatType: ChatType = .single) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)/read",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
]
)
}
public func setConversationPinned(targetId: String, chatType: ChatType, pinned: Bool) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)/pinned",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "pinned", value: String(pinned)),
]
)
}
public func setConversationMuted(targetId: String, chatType: ChatType, muted: Bool) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)/muted",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "muted", value: String(muted)),
]
)
}
public func setDraft(targetId: String, chatType: ChatType, draft: String) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)/draft",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "draft", value: draft),
]
)
}
public func deleteConversation(targetId: String, chatType: ChatType) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)",
method: "DELETE",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
]
)
}
public func getTotalUnreadCount() async throws -> Int {
let conversations = try await listConversations()
return conversations.reduce(0) { $0 + $1.unreadCount }
}
2026-04-21 22:07:29 +08:00
public func disconnect() {
client?.disconnect()
client = nil
}
private func fetchHistoryInternal(
toId: String? = nil,
groupId: String? = nil,
page: Int,
size: Int,
msgType: MsgType?,
keyword: String?,
startTime: Date?,
endTime: Date?,
isGroup: Bool = false
) async throws -> [ImMessage] {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "size", value: String(size)),
]
if let msgType { items.append(URLQueryItem(name: "msgType", value: msgType.rawValue)) }
if let keyword { items.append(URLQueryItem(name: "keyword", value: keyword)) }
if let startTime { items.append(URLQueryItem(name: "startTime", value: isoLocalDateTime(startTime))) }
if let endTime { items.append(URLQueryItem(name: "endTime", value: isoLocalDateTime(endTime))) }
let path = isGroup
? "/api/im/messages/group-history/\(groupId ?? "")"
: "/api/im/messages/history/\(toId ?? "")"
return try await ApiClient.shared.request(path: path, queryItems: items)
}
private func jsonString(from object: [String: Any?]) -> String {
var payload: [String: Any] = [:]
for (key, value) in object {
if let value { payload[key] = value }
}
guard JSONSerialization.isValidJSONObject(payload),
let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
let text = String(data: data, encoding: .utf8) else {
return "{}"
}
return text
}
private func isoLocalDateTime(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return formatter.string(from: date)
}
2026-04-21 22:07:29 +08:00
}