Tusker/Tusker/API/PostService.swift

137 lines
5.1 KiB
Swift

//
// 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: MastodonController
private let draft: Draft
let totalSteps: Int
@Published var currentStep = 1
init(mastodonController: MastodonController, draft: Draft) {
self.mastodonController = mastodonController
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: Preferences.shared.statusContentType,
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)
} 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 CompositionAttachmentData.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: CompositionAttachment) 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: CompositionAttachmentData.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
}
}
}
}