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 let clientFeaturesPublisher = PassthroughSubject<(XMPPClient, [String]), Never>() private var clientStateCancellables: Set = [] private var clientMessagesCancellables: Set = [] private var clientFeaturesCancellables: 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() } var clientFeatures: AnyPublisher<(XMPPClient, [String]), Never> { clientFeaturesPublisher.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 server features client.module(DiscoveryModule.self).$serverDiscoResult .sink { [weak self] disco in self?.clientFeaturesPublisher.send((client, disco.features)) } .store(in: &clientFeaturesCancellables) // subscribe to client messages client.module(MessageModule.self).messagesPublisher .sink { [weak self] message in self?.clientMessagesPublisher.send((client, message.message)) } .store(in: &clientMessagesCancellables) // subscribe to carbons client.module(MessageCarbonsModule.self).carbonsPublisher .sink { [weak self] carbon in self?.clientMessagesPublisher.send((client, carbon.message)) } .store(in: &clientMessagesCancellables) // subscribe to archived messages client.module(.mam).archivedMessagesPublisher .sink(receiveValue: { [weak self] archived in let message = archived.message message.attribute("archived_date", newValue: "\(archived.timestamp.timeIntervalSince1970)") self?.clientMessagesPublisher.send((client, message)) }) .store(in: &clientMessagesCancellables) // enable carbons if available client.module(.messageCarbons).$isAvailable.filter { $0 } .sink(receiveValue: { [weak client] _ in client?.module(.messageCarbons).enable() }) .store(in: &clientMessagesCancellables) // finally, do login 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(MessageArchiveManagementModule()) client.modulesManager.register(MessageCarbonsModule()) // file transfer modules client.modulesManager.register(HttpFileUploadModule()) // 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 } guard let chat = client.module(MessageModule.self).chatManager.chat(for: client.context, with: BareJID(to)) else { completion(false) return } let msg = chat.createMessage(text: message.body ?? "??", id: message.id) chat.send(message: msg) { res in switch res { case .success: completion(true) case .failure: completion(false) } } } func uploadAttachment(message: Message, completion: @escaping (Error?, String) -> Void) { guard let client = getClient(for: message.from), let to = message.to else { completion(XMPPError.bad_request("No such client"), "") return } guard let fileName = message.attachmentLocalName else { completion(XMPPError.bad_request("No such file"), "") return } let url = FileProcessing.fileFolder.appendingPathComponent(fileName) guard let data = try? Data(contentsOf: url) else { completion(XMPPError.bad_request("No such file"), "") return } guard let chat = client.module(MessageModule.self).chatManager.chat(for: client.context, with: BareJID(to)) else { completion(XMPPError.bad_request("No such chat"), "") return } let httpModule = client.module(HttpFileUploadModule.self) httpModule.findHttpUploadComponent { res in switch res { case .success(let components): guard let component = components.first(where: { $0.maxSize > data.count }) else { completion(XMPPError.bad_request("File too big"), "") return } httpModule.requestUploadSlot(componentJid: component.jid, filename: fileName, size: data.count, contentType: url.mimeType) { res in switch res { case .success(let slot): var request = URLRequest(url: slot.putUri) for (key, value) in slot.putHeaders { request.addValue(value, forHTTPHeaderField: key) } request.httpMethod = "PUT" request.httpBody = data request.addValue(String(data.count), forHTTPHeaderField: "Content-Length") request.addValue(url.mimeType, forHTTPHeaderField: "Content-Type") let session = URLSession(configuration: URLSessionConfiguration.default) session.dataTask(with: request) { _, response, error in let code = (response as? HTTPURLResponse)?.statusCode ?? 500 guard error == nil, code == 200 || code == 201 else { completion(XMPPError.bad_request("Upload failed"), "") return } if code == 200 { completion(XMPPError.bad_request("Invalid response code"), "") } else { let mesg = chat.createMessage(text: slot.getUri.absoluteString, id: message.id) mesg.oob = slot.getUri.absoluteString chat.send(message: mesg) { res in switch res { case .success: completion(nil, slot.getUri.absoluteString) case .failure: completion(XMPPError.bad_request("File uploaded, but message sent failed"), slot.getUri.absoluteString) } } } }.resume() case .failure(let error): completion(error, "") } } case .failure(let error): completion(error, "") } } } func requestArchivedMessages(jid: String, to: String?, fromDate: Date) { guard let client = getClient(for: jid) else { return } client.module(.mam).queryItems(componentJid: JID(jid), with: JID(to), start: fromDate, end: Date(), queryId: UUID().uuidString) { result in switch result { case .success(let response): print("MAM response: \(response)") case .failure(let error): print("MAM error: \(error)") } } } }