// // PostService.swift // Tusker // // Created by Shadowfacts on 4/27/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import Foundation import Pachyderm import UniformTypeIdentifiers class PostService: ObservableObject { private let mastodonController: ComposeMastodonContext private let config: ComposeUIConfig private let draft: Draft let totalSteps: Int @Published var currentStep = 1 init(mastodonController: ComposeMastodonContext, config: ComposeUIConfig, draft: Draft) { self.mastodonController = mastodonController self.config = config self.draft = draft // 2 steps (request data, then upload) for each attachment self.totalSteps = 2 + (draft.attachments.count * 2) } @MainActor func post() async throws { guard draft.hasContent else { return } // save before posting, so if a crash occurs during network request, the status won't be lost DraftsManager.save() let uploadedAttachments = try await uploadAttachments() let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil let sensitive = contentWarning != nil let request = Client.createStatus( text: textForPosting(), contentType: config.contentType, inReplyTo: draft.inReplyToID, media: uploadedAttachments, sensitive: sensitive, spoilerText: contentWarning, visibility: draft.visibility, language: nil, pollOptions: draft.poll?.options.map(\.text), pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration), pollMultiple: draft.poll?.multiple, localOnly: mastodonController.instanceFeatures.localOnlyPosts ? draft.localOnly : nil ) do { let (_, _) = try await mastodonController.run(request) currentStep += 1 DraftsManager.shared.remove(self.draft) DraftsManager.save() } catch let error as Client.Error { throw Error.posting(error) } } private func uploadAttachments() async throws -> [Attachment] { var attachments: [Attachment] = [] attachments.reserveCapacity(draft.attachments.count) for (index, attachment) in draft.attachments.enumerated() { let data: Data let utType: UTType do { (data, utType) = try await getData(for: attachment) currentStep += 1 } catch let error as AttachmentData.Error { throw Error.attachmentData(index: index, cause: error) } do { let uploaded = try await uploadAttachment(data: data, utType: utType, description: attachment.attachmentDescription) attachments.append(uploaded) currentStep += 1 } catch let error as Client.Error { throw Error.attachmentUpload(index: index, cause: error) } } return attachments } private func getData(for attachment: DraftAttachment) async throws -> (Data, UTType) { return try await withCheckedThrowingContinuation { continuation in attachment.data.getData(features: mastodonController.instanceFeatures) { result in switch result { case let .success(res): continuation.resume(returning: res) case let .failure(error): continuation.resume(throwing: error) } } } } private func uploadAttachment(data: Data, utType: UTType, description: String?) async throws -> Attachment { let formAttachment = FormAttachment(mimeType: utType.preferredMIMEType!, data: data, fileName: "file.\(utType.preferredFilenameExtension!)") let req = Client.upload(attachment: formAttachment, description: description) return try await mastodonController.run(req).0 } private func textForPosting() -> String { var text = draft.text // when using dictation, iOS sometimes leaves a U+FFFC OBJECT REPLACEMENT CHARACTER behind in the text, // which we want to strip out before actually posting the status text = text.replacingOccurrences(of: "\u{fffc}", with: "") if draft.localOnly && mastodonController.instanceFeatures.needsLocalOnlyEmojiHack { text += " 👁" } return text } enum Error: Swift.Error, LocalizedError { case attachmentData(index: Int, cause: AttachmentData.Error) case attachmentUpload(index: Int, cause: Client.Error) case posting(Client.Error) var localizedDescription: String { switch self { case let .attachmentData(index: index, cause: cause): return "Attachment \(index + 1): \(cause.localizedDescription)" case let .attachmentUpload(index: index, cause: cause): return "Attachment \(index + 1): \(cause.localizedDescription)" case let .posting(error): return error.localizedDescription } } } }