package com.xuqm.sdk.im import com.xuqm.sdk.XuqmLoginSession import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.im.api.AddMemberRequest import com.xuqm.sdk.im.api.CreateGroupRequest import com.xuqm.sdk.im.api.ImApi import com.xuqm.sdk.im.api.MuteGroupMemberRequest import com.xuqm.sdk.im.api.SetMutedRequest import com.xuqm.sdk.im.api.SetPinnedRequest import com.xuqm.sdk.im.api.SetGroupRoleRequest import com.xuqm.sdk.im.api.UpdateGroupRequest import com.xuqm.sdk.im.listener.ImEventListener import com.xuqm.sdk.im.model.ImConnectionState import com.xuqm.sdk.im.model.ConversationData import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.ImGroup import com.xuqm.sdk.im.model.ImMessage import com.xuqm.sdk.im.model.BlacklistEntry import com.xuqm.sdk.im.model.FriendRequest import com.xuqm.sdk.file.FileUploadResult import com.xuqm.sdk.network.ApiClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.json.JSONArray import org.json.JSONObject import java.time.LocalDateTime object ImSDK { private var client: ImClient? = null private val api: ImApi get() = ApiClient.create(ImApi::class.java, ServiceEndpointRegistry.imApiBaseUrl) private val connectionListener = object : ImEventListener { override fun onConnected() { _connectionState.value = ImConnectionState.Connected } override fun onDisconnected(reason: String?) { _connectionState.value = ImConnectionState.Disconnected(reason) } override fun onError(error: String) { _connectionState.value = ImConnectionState.Disconnected(error) } } private val _connectionState = MutableStateFlow(ImConnectionState.Disconnected("未连接")) val connectionState: StateFlow = _connectionState var currentUserId: String = "" private set init { XuqmSDK.currentLoginSession?.let { onSdkLogin(it) } } suspend fun login( userId: String, userSig: String, nickname: String? = null, avatar: String? = null, ) = withContext(Dispatchers.IO) { XuqmSDK.requireInit() currentUserId = userId connectWithToken(userSig) } suspend fun loginWithUserSig(userId: String, userSig: String) = withContext(Dispatchers.IO) { XuqmSDK.requireInit() currentUserId = userId connectWithToken(userSig) } @Deprecated("Use loginWithUserSig(userId, userSig) instead.") suspend fun loginWithToken(userId: String, token: String) = loginWithUserSig(userId, token) fun sendMessage( toId: String, chatType: String, msgType: String, content: String, mentionedUserIds: String? = null, ) { client?.sendMessage(toId, chatType, msgType, content, mentionedUserIds) } fun sendTextMessage( toId: String, chatType: String, content: String, mentionedUserIds: String? = null, ) { sendMessage(toId, chatType, "TEXT", content, mentionedUserIds) } fun sendImageMessage( toId: String, chatType: String, file: FileUploadResult, width: Int? = null, height: Int? = null, ) { sendMessage( toId = toId, chatType = chatType, msgType = "IMAGE", content = buildMediaPayload( url = file.url, thumbnailUrl = file.thumbnailUrl, name = file.originalName, mimeType = file.mimeType, size = file.size, hash = file.hash, width = width, height = height, ), ) } fun sendVideoMessage( toId: String, chatType: String, file: FileUploadResult, width: Int? = null, height: Int? = null, durationMs: Long? = null, ) { sendMessage( toId = toId, chatType = chatType, msgType = "VIDEO", content = buildMediaPayload( url = file.url, thumbnailUrl = file.thumbnailUrl, name = file.originalName, mimeType = file.mimeType, size = file.size, hash = file.hash, width = width, height = height, durationMs = durationMs, ), ) } fun sendFileMessage( toId: String, chatType: String, file: FileUploadResult, ) { sendMessage( toId = toId, chatType = chatType, msgType = "FILE", content = buildMediaPayload( url = file.url, name = file.originalName, mimeType = file.mimeType, size = file.size, hash = file.hash, ), ) } fun sendAudioMessage( toId: String, chatType: String, file: FileUploadResult, durationMs: Long? = null, ) { sendMessage( toId = toId, chatType = chatType, msgType = "AUDIO", content = buildMediaPayload( url = file.url, name = file.originalName, mimeType = file.mimeType, size = file.size, hash = file.hash, durationMs = durationMs, ), ) } fun sendLocationMessage( toId: String, chatType: String, latitude: Double, longitude: Double, title: String? = null, address: String? = null, ) { sendMessage( toId = toId, chatType = chatType, msgType = "LOCATION", content = JSONObject().apply { put("lat", latitude) put("lng", longitude) title?.takeIf { it.isNotBlank() }?.let { put("title", it) } address?.takeIf { it.isNotBlank() }?.let { put("address", it) } }.toString(), ) } fun sendCustomMessage( toId: String, chatType: String, data: String, ) { sendMessage( toId = toId, chatType = chatType, msgType = "CUSTOM", content = JSONObject().apply { put("data", data) }.toString(), ) } fun sendRichTextMessage( toId: String, chatType: String, html: String, ) { sendMessage( toId = toId, chatType = chatType, msgType = "RICH_TEXT", content = JSONObject().apply { put("html", html) }.toString(), ) } fun sendForwardMessage( toId: String, chatType: String, originalSender: String, originalContent: String, ) { sendMessage( toId = toId, chatType = chatType, msgType = "FORWARD", content = JSONObject().apply { put("originalSender", originalSender) put("originalContent", originalContent) }.toString(), ) } fun sendCallAudioMessage( toId: String, chatType: String, action: String, ) { sendCallSignalMessage(toId, chatType, "CALL_AUDIO", action) } fun sendCallVideoMessage( toId: String, chatType: String, action: String, ) { sendCallSignalMessage(toId, chatType, "CALL_VIDEO", action) } fun sendNotifyMessage( toId: String, chatType: String, title: String, content: String, ) { sendMessage( toId = toId, chatType = chatType, msgType = "NOTIFY", content = JSONObject().apply { put("title", title) put("content", content) }.toString(), ) } fun sendQuoteMessage( toId: String, chatType: String, quotedMsgId: String, quotedContent: String, text: String, ) { sendMessage( toId = toId, chatType = chatType, msgType = "QUOTE", content = JSONObject().apply { put("quotedMsgId", quotedMsgId) put("quotedContent", quotedContent) put("text", text) }.toString(), ) } fun sendMergeMessage( toId: String, chatType: String, title: String, msgList: List, ) { sendMessage( toId = toId, chatType = chatType, msgType = "MERGE", content = JSONObject().apply { put("title", title) put("msgList", JSONArray(msgList)) }.toString(), ) } fun subscribeGroup(groupId: String) { client?.subscribe("/topic/group/$groupId") } fun unsubscribeGroup(groupId: String) { client?.unsubscribe("/topic/group/$groupId") } suspend fun fetchHistory(toId: String, page: Int = 0, size: Int = 20): List = fetchHistoryWithFilters(toId, page, size) suspend fun fetchHistoryWithFilters( toId: String, page: Int = 0, size: Int = 20, msgType: String? = null, keyword: String? = null, startTime: LocalDateTime? = null, endTime: LocalDateTime? = null, ): List = withContext(Dispatchers.IO) { api.fetchHistory( toId, XuqmSDK.appId, msgType, keyword, startTime?.toString(), endTime?.toString(), page, size, ).data ?: emptyList() } suspend fun fetchGroupHistory(groupId: String, page: Int = 0, size: Int = 20): List = fetchGroupHistoryWithFilters(groupId, page, size) suspend fun fetchGroupHistoryWithFilters( groupId: String, page: Int = 0, size: Int = 20, msgType: String? = null, keyword: String? = null, startTime: LocalDateTime? = null, endTime: LocalDateTime? = null, ): List = withContext(Dispatchers.IO) { api.fetchGroupHistory( groupId, XuqmSDK.appId, msgType, keyword, startTime?.toString(), endTime?.toString(), page, size, ).data ?: emptyList() } suspend fun listGroups(): List = withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() } suspend fun createGroup(name: String, memberIds: List, groupType: String = "WORK"): ImGroup? = withContext(Dispatchers.IO) { api.createGroup(XuqmSDK.appId, CreateGroupRequest(name, memberIds, groupType)).data } suspend fun getGroupInfo(groupId: String): ImGroup? = withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data } suspend fun listPublicGroups(keyword: String? = null): List = withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appId, keyword).data ?: emptyList() } suspend fun updateGroupInfo(groupId: String, name: String? = null, announcement: String? = null) = withContext(Dispatchers.IO) { api.updateGroupInfo(groupId, UpdateGroupRequest(name, announcement)) } suspend fun addGroupMember(groupId: String, userId: String) = withContext(Dispatchers.IO) { api.addGroupMember(groupId, AddMemberRequest(userId)) } suspend fun removeGroupMember(groupId: String, userId: String) = withContext(Dispatchers.IO) { api.removeGroupMember(groupId, userId) } suspend fun leaveGroup(groupId: String) = withContext(Dispatchers.IO) { api.removeGroupMember(groupId, currentUserId) } suspend fun setGroupRole(groupId: String, userId: String, role: String) = withContext(Dispatchers.IO) { api.setGroupRole(groupId, SetGroupRoleRequest(userId, role)) } suspend fun muteGroupMember(groupId: String, userId: String, minutes: Long) = withContext(Dispatchers.IO) { api.muteGroupMember(groupId, MuteGroupMemberRequest(userId, minutes)) } suspend fun dismissGroup(groupId: String) = withContext(Dispatchers.IO) { api.dismissGroup(groupId) } suspend fun sendGroupJoinRequest(groupId: String, remark: String? = null): GroupJoinRequest? = withContext(Dispatchers.IO) { api.sendGroupJoinRequest(groupId, XuqmSDK.appId, remark).data } suspend fun listGroupJoinRequests(groupId: String): List = withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appId).data ?: emptyList() } suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? = withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data } suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? = withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data } suspend fun listFriends(): List = withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appId).data ?: emptyList() } suspend fun addFriend(friendId: String) = withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appId, friendId) } suspend fun removeFriend(friendId: String) = withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appId) } suspend fun listFriendRequests(direction: String = "incoming"): List = withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appId, direction).data ?: emptyList() } suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? = withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appId, friendId, remark).data } suspend fun acceptFriendRequest(requestId: String): FriendRequest? = withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appId).data } suspend fun rejectFriendRequest(requestId: String): FriendRequest? = withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appId).data } suspend fun listBlacklist(): List = withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appId).data ?: emptyList() } suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? = withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appId, blockedUserId).data } suspend fun removeFromBlacklist(blockedUserId: String) = withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appId, blockedUserId) } suspend fun listConversations(): List = withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() } suspend fun getTotalUnreadCount(): Int = withContext(Dispatchers.IO) { runCatching { listConversations().sumOf { it.unreadCount } }.getOrDefault(0) } suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) = withContext(Dispatchers.IO) { api.setConversationPinned(targetId, chatType, SetPinnedRequest(pinned)) } suspend fun setConversationMuted(targetId: String, chatType: String, muted: Boolean) = withContext(Dispatchers.IO) { api.setConversationMuted(targetId, chatType, SetMutedRequest(muted)) } suspend fun markRead(targetId: String, chatType: String = "SINGLE") = withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) } suspend fun setDraft(targetId: String, chatType: String, draft: String) = withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appId, chatType, draft) } suspend fun deleteConversation(targetId: String, chatType: String) = withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appId, chatType) } fun addListener(listener: ImEventListener) = client?.addListener(listener) fun removeListener(listener: ImEventListener) = client?.removeListener(listener) fun disconnect() { disconnectInternal(clearTokenStore = true) } fun onSdkLogin(session: XuqmLoginSession) { XuqmSDK.requireInit() currentUserId = session.userId connectWithToken(session.userSig) } fun onSdkLogout() { disconnectInternal(clearTokenStore = false) } private fun connectWithToken(token: String) { XuqmSDK.tokenStore.saveToken(token) client?.disconnect() _connectionState.value = ImConnectionState.Connecting client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId) client?.addListener(connectionListener) client?.connect() } private fun disconnectInternal(clearTokenStore: Boolean) { client?.disconnect() client = null currentUserId = "" _connectionState.value = ImConnectionState.Disconnected("已断开") if (clearTokenStore) { XuqmSDK.tokenStore.clear() } } private fun buildMediaPayload( url: String, thumbnailUrl: String? = null, name: String? = null, mimeType: String? = null, size: Long? = null, hash: String? = null, width: Int? = null, height: Int? = null, durationMs: Long? = null, ): String = JSONObject().apply { put("url", url) thumbnailUrl?.takeIf { it.isNotBlank() }?.let { put("thumbnailUrl", it) } name?.takeIf { it.isNotBlank() }?.let { put("name", it) } mimeType?.takeIf { it.isNotBlank() }?.let { put("mimeType", it) } size?.takeIf { it > 0 }?.let { put("size", it) } hash?.takeIf { it.isNotBlank() }?.let { put("hash", it) } width?.takeIf { it > 0 }?.let { put("width", it) } height?.takeIf { it > 0 }?.let { put("height", it) } durationMs?.takeIf { it > 0 }?.let { put("durationMs", it) } }.toString() private fun sendCallSignalMessage( toId: String, chatType: String, msgType: String, action: String, ) { sendMessage( toId = toId, chatType = chatType, msgType = msgType, content = JSONObject().apply { put("action", action) }.toString(), ) } }