// // GroupIMService.swift // QuickLocation // // Created by 八条 on 2026/6/4. // import Foundation import OpenIMSDK import RxSwift final class GroupIMService { static let shared = GroupIMService() private var isInited = false private var isLogining = false private var isLoggingOut = false private var isSessionInvalidated = false private var loginGeneration = 0 /// 登录进行中时排队的回调,登录结束统一回调,避免并发重复登录 private var pendingLoginCompletions: [(userId: String, completion: (Bool) -> Void)] = [] private var pendingLogoutCompletions: [(Bool) -> Void] = [] private let disposeBag = DisposeBag() private init() {} // MARK: - Init SDK func initSDK() { // 幂等:SDK 全局只初始化一次 guard !isInited else { return } isInited = true let config = OIMInitConfig() config.apiAddr = URLManager.shared.openIM_API config.wsAddr = URLManager.shared.openIM_WS config.platform = .iPhone OIMManager.manager.initSDK(with: config, onConnecting: {}, onConnectFailure: { _, _ in }, onConnectSuccess: {}, onKickedOffline: { GroupIMService.shared.invalidateSession() }, onUserTokenExpired: { GroupIMService.shared.invalidateSession() }, onUserTokenInvalid: { _ in GroupIMService.shared.invalidateSession() }) } // MARK: - Login /// 确保已登录(幂等):已登录直接回调,否则拉 token 再登录;登录中则排队等待 func ensureLogin(completion: @escaping (Bool) -> Void) { runOnMain { [weak self] in self?.login(completion: completion) } } private func login(completion: @escaping (Bool) -> Void) { if AppContextManager.shared.isGuest { completion(false) return } let userId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !userId.isEmpty else { completion(false) return } if !isSessionInvalidated, OIMManager.manager.getLoginStatus() == .logged, OIMManager.manager.getLoginUserID() == userId { completion(true) return } pendingLoginCompletions.append((userId, completion)) startLoginIfNeeded() } private func startLoginIfNeeded() { guard !isLogining, !isLoggingOut, !pendingLoginCompletions.isEmpty else { return } let userId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !AppContextManager.shared.isGuest, !userId.isEmpty else { let callbacks = pendingLoginCompletions pendingLoginCompletions.removeAll() callbacks.forEach { $0.completion(false) } return } let staleCompletions = pendingLoginCompletions.filter { $0.userId != userId } pendingLoginCompletions.removeAll { $0.userId != userId } staleCompletions.forEach { $0.completion(false) } guard !pendingLoginCompletions.isEmpty else { return } let status = OIMManager.manager.getLoginStatus() if status == .logged { if !isSessionInvalidated, OIMManager.manager.getLoginUserID() == userId { finishLogin(success: true, userId: userId) } else { clearCachedToken() loginGeneration += 1 beginSDKLogout() } return } if status == .logging { clearCachedToken() loginGeneration += 1 beginSDKLogout() return } isLogining = true loginGeneration += 1 let generation = loginGeneration let existing = AppContextManager.shared.imToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !existing.isEmpty, AppContextManager.shared.imTokenUserId == userId { performSDKLogin(userId: userId, token: existing, generation: generation, canRefreshToken: true) return } clearCachedToken() requestTokenAndLogin(userId: userId, generation: generation) } private func requestTokenAndLogin(userId: String, generation: Int) { UserService.imToken() .subscribe(onNext: { [weak self] response in guard let self else { return } guard self.loginGeneration == generation else { return } guard AppContextManager.shared.userId == userId else { self.finishLogin(success: false, userId: userId) return } guard let data = response.data, let token = data["token"] as? String, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { self.finishLogin(success: false, userId: userId) return } let normalizedToken = token.trimmingCharacters(in: .whitespacesAndNewlines) AppContextManager.shared.imToken = normalizedToken AppContextManager.shared.imTokenUserId = userId self.performSDKLogin(userId: userId, token: normalizedToken, generation: generation, canRefreshToken: false) }, onError: { [weak self] _ in guard let self, self.loginGeneration == generation else { return } self.finishLogin(success: false, userId: userId) }) .disposed(by: disposeBag) } private func performSDKLogin(userId: String, token: String, generation: Int, canRefreshToken: Bool) { OIMManager.manager.login(userId, token: token) { [weak self] _ in guard let self, self.loginGeneration == generation else { return } let isCurrentUser = AppContextManager.shared.userId == userId && OIMManager.manager.getLoginUserID() == userId self.isSessionInvalidated = !isCurrentUser self.finishLogin(success: isCurrentUser, userId: userId) } onFailure: { [weak self] code, msg in guard let self, self.loginGeneration == generation else { return } print("OpenIM login failed: \(code) \(msg ?? "")") if canRefreshToken, AppContextManager.shared.userId == userId { self.clearCachedToken() self.requestTokenAndLogin(userId: userId, generation: generation) } else { self.finishLogin(success: false, userId: userId) } } } private func finishLogin(success: Bool, userId: String) { isLogining = false let callbacks = pendingLoginCompletions.filter { $0.userId == userId } pendingLoginCompletions.removeAll { $0.userId == userId } callbacks.forEach { $0.completion(success) } startLoginIfNeeded() } // MARK: - Logout func logout(completion: ((Bool) -> Void)? = nil) { runOnMain { [weak self] in guard let self else { return } self.clearCachedToken() self.isSessionInvalidated = true self.loginGeneration += 1 self.isLogining = false let loginCallbacks = self.pendingLoginCompletions self.pendingLoginCompletions.removeAll() loginCallbacks.forEach { $0.completion(false) } if let completion { self.pendingLogoutCompletions.append(completion) } self.beginSDKLogout() } } private func beginSDKLogout() { guard !isLoggingOut else { return } guard OIMManager.manager.getLoginStatus() != .logout else { finishLogout(success: true) return } isLoggingOut = true OIMManager.manager.logoutWith(onSuccess: { (_: String?) in GroupIMService.shared.finishLogout(success: true) }, onFailure: { [weak self] (code: Int, msg: String?) in print("OpenIM logout failed: \(code) \(msg ?? "")") self?.finishLogout(success: false) }) } private func finishLogout(success: Bool) { isLoggingOut = false let callbacks = pendingLogoutCompletions pendingLogoutCompletions.removeAll() callbacks.forEach { $0(success) } if success { startLoginIfNeeded() } else { let loginCallbacks = pendingLoginCompletions pendingLoginCompletions.removeAll() loginCallbacks.forEach { $0.completion(false) } } } private func clearCachedToken() { AppContextManager.shared.imToken = nil AppContextManager.shared.imTokenUserId = nil } private func invalidateSession() { runOnMain { [weak self] in self?.isSessionInvalidated = true self?.clearCachedToken() } } private func runOnMain(_ action: @escaping () -> Void) { if Thread.isMainThread { action() } else { DispatchQueue.main.async(execute: action) } } // MARK: - Send func sendGroupMessage(groupId: String, makeMessage: @escaping () -> OIMMessageInfo, onPrepared: ((OIMMessageInfo) -> Void)? = nil, onSuccess: ((OIMMessageInfo) -> Void)?, onProgress: ((Int) -> Void)? = nil, onFailure: ((Int, String?) -> Void)?) { let normalizedGroupId = groupId.trimmingCharacters(in: .whitespacesAndNewlines) guard !normalizedGroupId.isEmpty else { onFailure?(-1, "圈子信息无效") return } ensureLogin { success in let userId = AppContextManager.shared.userId guard success, OIMManager.manager.getLoginStatus() == .logged, OIMManager.manager.getLoginUserID() == userId else { onFailure?(-1, "消息服务连接失败,请重试") return } let message = makeMessage() guard !(message.clientMsgID ?? "").isEmpty else { onFailure?(-1, "消息创建失败,请重试") return } onPrepared?(message) OIMManager.manager.sendMessage(message, recvID: "", groupID: normalizedGroupId, offlinePushInfo: nil, onSuccess: { returnedMessage in onSuccess?(returnedMessage ?? message) }, onProgress: { progress in onProgress?(progress) }, onFailure: { code, errorMessage in onFailure?(code, errorMessage) }) } } func displaySendError(_ message: String?, fallback: String) -> String { let text = message?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !text.isEmpty else { return fallback } let lowercased = text.lowercased() if lowercased.contains("json") || lowercased.contains("unexpected end") { return fallback } return text } // MARK: - Get Joined Groups func getJoinedGroups(completion: @escaping ([OIMGroupInfo]) -> Void) { OIMManager.manager.getJoinedGroupListWith(onSuccess: { groups in completion(groups ?? []) }, onFailure: { code, msg in print("GroupIMService: getJoinedGroups failed: \(code) \(msg ?? "")") completion([]) }) } // MARK: - Conversation List (for unread count & last msg time) func getConversationList(completion: @escaping ([OIMConversationInfo]) -> Void) { OIMManager.manager.getAllConversationListWith(onSuccess: { list in completion(list ?? []) }, onFailure: { code, msg in completion([]) }) } // MARK: - Conversation Listener private var conversationListener: ConversationListenerProxy? func setConversationListener(_ handler: @escaping ([OIMConversationInfo]) -> Void) { conversationListener = ConversationListenerProxy(handler: handler) OIMManager.callbacker.addConversationListener(listener: conversationListener!) } // MARK: - Group Listener private var groupListener: GroupListenerProxy? /// 监听群组变化(加群/退群/群信息变更),用于 SDK 同步到新群时刷新列表 func setGroupListener(_ handler: @escaping () -> Void) { groupListener = GroupListenerProxy(handler: handler) OIMManager.callbacker.addGroupListener(listener: groupListener!) } } // MARK: - GroupListenerProxy private class GroupListenerProxy: NSObject, OIMGroupListener { private let handler: () -> Void init(handler: @escaping () -> Void) { self.handler = handler } func onJoinedGroupAdded(_ groupInfo: OIMGroupInfo) { handler() } func onJoinedGroupDeleted(_ groupInfo: OIMGroupInfo) { handler() } func onGroupInfoChanged(_ changeInfo: OIMGroupInfo) { handler() } } // MARK: - ConversationListenerProxy private class ConversationListenerProxy: NSObject, OIMConversationListener { private let handler: ([OIMConversationInfo]) -> Void init(handler: @escaping ([OIMConversationInfo]) -> Void) { self.handler = handler } func onConversationChanged(_ conversations: [OIMConversationInfo]) { handler(conversations) } func onNewConversation(_ conversations: [OIMConversationInfo]) { handler(conversations) } }