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 } 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 { completion(nil, slot.getUri.absoluteString) } }.resume() case .failure: completion(XMPPError.bad_request("Upload failed"), "") } } case .failure: completion(XMPPError.bad_request("No such component"), "") } } } } // open class HTTPFileUploadHelper { // // private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HTTPFileUploadHelper") // // public static func upload(for context: Context, filename: String, inputStream: InputStream, filesize size: Int, mimeType: String, delegate: URLSessionDelegate?, completionHandler: @escaping (Result)->Void) { // let httpUploadModule = context.module(.httpFileUpload); // httpUploadModule.findHttpUploadComponent(completionHandler: { result in // switch result { // case .success(let components): // guard let component = components.first(where: { $0.maxSize > size }) else { // completionHandler(.failure(.fileTooBig)); // return; // } // httpUploadModule.requestUploadSlot(componentJid: component.jid, filename: filename, size: size, contentType: mimeType, completionHandler: { result in // switch result { // case .success(let slot): // var request = URLRequest(url: slot.putUri); // slot.putHeaders.forEach({ (k,v) in // request.addValue(v, forHTTPHeaderField: k); // }); // request.httpMethod = "PUT"; // request.httpBodyStream = inputStream; // request.addValue(String(size), forHTTPHeaderField: "Content-Length"); // request.addValue(mimeType, forHTTPHeaderField: "Content-Type"); // let session = URLSession(configuration: URLSessionConfiguration.default, delegate: delegate, delegateQueue: OperationQueue.main); // session.dataTask(with: request) { (data, response, error) in // let code = (response as? HTTPURLResponse)?.statusCode ?? 500; // guard error == nil && (code == 200 || code == 201) else { // logger.error("upload of file \(filename) failed, error: \(error as Any), response: \(response as Any)"); // completionHandler(.failure(.httpError)); // return; // } // if code == 200 { // completionHandler(.failure(.invalidResponseCode(url: slot.getUri))); // } else { // completionHandler(.success(slot.getUri)); // } // }.resume(); // case .failure(let error): // logger.error("upload of file \(filename) failed, upload component returned error: \(error as Any)"); // completionHandler(.failure(.unknownError)); // } // }); // case .failure(let error): // completionHandler(.failure(error.errorCondition == .item_not_found ? .notSupported : .unknownError)); // } // }) // } // // public enum UploadResult { // case success(url: URL, filesize: Int, mimeType: String?) // case failure(ShareError) // } // }