2024-06-19 15:15:27 +00:00
|
|
|
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>()
|
2024-06-24 10:44:55 +00:00
|
|
|
private let clientMessagesPublisher = PassthroughSubject<(XMPPClient, Martin.Message), Never>()
|
|
|
|
private var clientStateCancellables: Set<AnyCancellable> = []
|
|
|
|
private var clientMessagesCancellables: Set<AnyCancellable> = []
|
2024-06-19 15:15:27 +00:00
|
|
|
|
|
|
|
@Published private(set) var clients: [XMPPClient] = []
|
|
|
|
var clientState: AnyPublisher<(XMPPClient, XMPPClient.State), Never> {
|
|
|
|
clientStatePublisher.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2024-06-24 10:44:55 +00:00
|
|
|
var clientMessages: AnyPublisher<(XMPPClient, Martin.Message), Never> {
|
|
|
|
clientMessagesPublisher.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2024-06-19 15:15:27 +00:00
|
|
|
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 {
|
2024-06-24 10:44:55 +00:00
|
|
|
// add client
|
2024-06-19 15:15:27 +00:00
|
|
|
let client = makeClient(for: account, with: manager)
|
|
|
|
clients.append(client)
|
2024-06-24 10:44:55 +00:00
|
|
|
|
|
|
|
// subscribe to client state
|
|
|
|
client.$state
|
2024-06-19 15:15:27 +00:00
|
|
|
.sink { [weak self] state in
|
|
|
|
self?.clientStatePublisher.send((client, state))
|
|
|
|
}
|
2024-06-24 10:44:55 +00:00
|
|
|
.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)
|
2024-06-19 15:15:27 +00:00
|
|
|
|
|
|
|
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())
|
|
|
|
|
2024-07-14 18:28:54 +00:00
|
|
|
// file transfer modules
|
|
|
|
client.modulesManager.register(HttpFileUploadModule())
|
|
|
|
|
2024-06-19 15:15:27 +00:00
|
|
|
// 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 }
|
|
|
|
}
|
2024-06-25 12:20:20 +00:00
|
|
|
|
2024-06-26 10:26:04 +00:00
|
|
|
func sendMessage(message: Message, completion: @escaping (Bool) -> Void) {
|
|
|
|
guard let client = getClient(for: message.from), let to = message.to else {
|
|
|
|
completion(false)
|
|
|
|
return
|
|
|
|
}
|
2024-07-02 07:22:28 +00:00
|
|
|
guard let chat = client.module(MessageModule.self).chatManager.chat(for: client.context, with: BareJID(to)) else {
|
|
|
|
completion(false)
|
|
|
|
return
|
|
|
|
}
|
2024-06-26 10:26:04 +00:00
|
|
|
|
2024-07-02 07:22:28 +00:00
|
|
|
let msg = chat.createMessage(text: message.body ?? "??", id: message.id)
|
|
|
|
chat.send(message: msg) { res in
|
2024-07-01 09:29:31 +00:00
|
|
|
switch res {
|
|
|
|
case .success:
|
|
|
|
completion(true)
|
|
|
|
|
|
|
|
case .failure:
|
|
|
|
completion(false)
|
2024-06-26 10:26:04 +00:00
|
|
|
}
|
2024-07-01 09:29:31 +00:00
|
|
|
}
|
2024-06-25 12:20:20 +00:00
|
|
|
}
|
2024-07-14 18:28:54 +00:00
|
|
|
|
|
|
|
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"), "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-06-19 15:15:27 +00:00
|
|
|
}
|
2024-07-09 12:37:51 +00:00
|
|
|
|
|
|
|
// 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<URL,ShareError>)->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)
|
|
|
|
// }
|
|
|
|
// }
|