import Combine import Foundation import GRDB import Martin protocol MartinsManager: Martin.RosterManager & Martin.ChatManager {} final class XMPPService: ObservableObject { private let manager: MartinsManager private let clientStatePublisher = PassthroughSubject<(XMPPClient, XMPPClient.State), Never>() private let clientMessagesPublisher = PassthroughSubject<(XMPPClient, Martin.Message), Never>() private var clientStateCancellables: Set = [] private var clientMessagesCancellables: Set = [] @Published private(set) var clients: [XMPPClient] = [] var clientState: AnyPublisher<(XMPPClient, XMPPClient.State), Never> { clientStatePublisher.eraseToAnyPublisher() } var clientMessages: AnyPublisher<(XMPPClient, Martin.Message), Never> { clientMessagesPublisher.eraseToAnyPublisher() } init(manager: MartinsManager) { self.manager = manager } func updateClients(for accounts: [Account]) { // get simple diff let forAdd = accounts .filter { !self.clients.map { $0.connectionConfiguration.userJid.stringValue }.contains($0.bareJid) } let forRemove = clients .map { $0.connectionConfiguration.userJid.stringValue } .filter { !accounts.map { $0.bareJid }.contains($0) } // init and add clients for account in forAdd { // add client let client = makeClient(for: account, with: manager) clients.append(client) // subscribe to client state client.$state .sink { [weak self] state in self?.clientStatePublisher.send((client, state)) } .store(in: &clientStateCancellables) // subscribe to client messages client.module(MessageModule.self).messagesPublisher .sink { [weak self] message in self?.clientMessagesPublisher.send((client, message.message)) } .store(in: &clientMessagesCancellables) client.login() } // remove clients for jid in forRemove { deinitClient(jid: jid) } } private func makeClient(for account: Account, with manager: MartinsManager) -> XMPPClient { let client = XMPPClient() // register modules // core modules RFC 6120 client.modulesManager.register(StreamFeaturesModule()) client.modulesManager.register(SaslModule()) client.modulesManager.register(AuthModule()) client.modulesManager.register(SessionEstablishmentModule()) client.modulesManager.register(ResourceBinderModule()) client.modulesManager.register(DiscoveryModule(identity: .init(category: "client", type: "iOS", name: Const.appName))) // messaging modules RFC 6121 client.modulesManager.register(RosterModule(rosterManager: manager)) client.modulesManager.register(PresenceModule()) client.modulesManager.register(PubSubModule()) client.modulesManager.register(MessageModule(chatManager: manager)) client.modulesManager.register(MessageCarbonsModule()) client.modulesManager.register(MessageArchiveManagementModule()) // extensions client.modulesManager.register(SoftwareVersionModule()) client.modulesManager.register(PingModule()) client.connectionConfiguration.userJid = .init(account.bareJid) client.connectionConfiguration.credentials = .password(password: account.pass) // add client to clients return client } func deinitClient(jid: String) { if let index = clients.firstIndex(where: { $0.connectionConfiguration.userJid.stringValue == jid }) { let client = clients.remove(at: index) _ = client.disconnect() } } func getClient(for jid: String) -> XMPPClient? { clients.first { $0.connectionConfiguration.userJid.stringValue == jid } } func sendMessage(message: Message, completion: @escaping (Bool) -> Void) { guard let client = getClient(for: message.from), let to = message.to else { completion(false) return } let message = Martin.Message() message.to = JID(to) message.body = message.body client.module(MessageModule.self) .chatManager .chat(for: client.context, with: JID(to).bareJid)? .send(message: message) { res in switch res { case .success: completion(true) case .failure: completion(false) } } } }