382 lines
14 KiB
Swift
382 lines
14 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|