jsdw_ios/QuickLocation/Section/Group/GroupIMService.swift

382 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)
}
}