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()) // 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 (k, v) in slot.putHeaders { request.addValue(v, forHTTPHeaderField: k) } 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, "") } } } }