diff --git a/ConversationsClassic/AppCore/Actions/XMPPActions.swift b/ConversationsClassic/AppCore/Actions/XMPPActions.swift index bc3599e..7bde93c 100644 --- a/ConversationsClassic/AppCore/Actions/XMPPActions.swift +++ b/ConversationsClassic/AppCore/Actions/XMPPActions.swift @@ -5,4 +5,9 @@ enum XMPPAction: Codable { case xmppMessageSent(Message) case xmppMessageSendFailed(msgId: String) case xmppMessageSendSuccess(msgId: String) + + case xmppAttachmentUpload(Message) + // case xmppAttachmentSlotRequestDone(String) //TODO: ??? + case xmppAttachmentUploadFailed(msgId: String, reason: String) + case xmppAttachmentUploadSuccess(msgId: String, attachmentRemotePath: String) } diff --git a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift index 25e13c3..c7f2a5a 100644 --- a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift @@ -100,6 +100,20 @@ final class XMPPMiddleware { } .eraseToAnyPublisher() + case .xmppAction(.xmppAttachmentUpload(let message)): + return Future { [weak self] promise in + DispatchQueue.global().async { + self?.service.uploadAttachment(message: message) { done, remotePath in + if done { + promise(.success(.xmppAction(.xmppAttachmentUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath)))) + } else { + promise(.success(.xmppAction(.xmppAttachmentUploadFailed(msgId: message.id, reason: "Upload failed")))) + } + } + } + } + .eraseToAnyPublisher() + default: return Empty().eraseToAnyPublisher() } diff --git a/ConversationsClassic/AppCore/XMPP/XMPPService.swift b/ConversationsClassic/AppCore/XMPP/XMPPService.swift index 360ea12..68991fd 100644 --- a/ConversationsClassic/AppCore/XMPP/XMPPService.swift +++ b/ConversationsClassic/AppCore/XMPP/XMPPService.swift @@ -83,6 +83,9 @@ final class XMPPService: ObservableObject { client.modulesManager.register(MessageCarbonsModule()) client.modulesManager.register(MessageArchiveManagementModule()) + // file transfer modules + client.modulesManager.register(HttpFileUploadModule()) + // extensions client.modulesManager.register(SoftwareVersionModule()) client.modulesManager.register(PingModule()) @@ -125,6 +128,65 @@ final class XMPPService: ObservableObject { } } } + + 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 { diff --git a/ConversationsClassic/Helpers/Date+Extensions.swift b/ConversationsClassic/Helpers/TimeInterval+Extensions.swift similarity index 100% rename from ConversationsClassic/Helpers/Date+Extensions.swift rename to ConversationsClassic/Helpers/TimeInterval+Extensions.swift diff --git a/ConversationsClassic/Helpers/URL+Extensions.swift b/ConversationsClassic/Helpers/URL+Extensions.swift new file mode 100644 index 0000000..c84c26a --- /dev/null +++ b/ConversationsClassic/Helpers/URL+Extensions.swift @@ -0,0 +1,13 @@ +import UniformTypeIdentifiers + +extension URL { + var mimeType: String { + let pathExtension = self.pathExtension + + if let uti = UTType(filenameExtension: pathExtension) { + return uti.preferredMIMEType ?? "application/octet-stream" + } else { + return "application/octet-stream" + } + } +}