2023-04-16 17:23:13 +00:00
|
|
|
//
|
|
|
|
// PostService.swift
|
2023-04-18 00:03:31 +00:00
|
|
|
// ComposeUI
|
2023-04-16 17:23:13 +00:00
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 4/27/22.
|
|
|
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import Pachyderm
|
|
|
|
import UniformTypeIdentifiers
|
|
|
|
|
2023-04-23 02:03:52 +00:00
|
|
|
@MainActor
|
2023-04-16 17:23:13 +00:00
|
|
|
class PostService: ObservableObject {
|
|
|
|
private let mastodonController: ComposeMastodonContext
|
|
|
|
private let config: ComposeUIConfig
|
|
|
|
private let draft: Draft
|
|
|
|
|
|
|
|
@Published var currentStep = 1
|
2023-05-11 13:59:57 +00:00
|
|
|
@Published private(set) var totalSteps = 2
|
2023-04-16 17:23:13 +00:00
|
|
|
|
|
|
|
init(mastodonController: ComposeMastodonContext, config: ComposeUIConfig, draft: Draft) {
|
|
|
|
self.mastodonController = mastodonController
|
|
|
|
self.config = config
|
|
|
|
self.draft = draft
|
|
|
|
}
|
|
|
|
|
|
|
|
func post() async throws {
|
2023-05-11 18:39:49 +00:00
|
|
|
guard draft.hasContent || draft.editedStatusID != nil else {
|
2023-04-16 17:23:13 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// save before posting, so if a crash occurs during network request, the status won't be lost
|
2023-04-23 01:16:30 +00:00
|
|
|
DraftsPersistentContainer.shared.save()
|
2023-04-16 17:23:13 +00:00
|
|
|
|
|
|
|
let uploadedAttachments = try await uploadAttachments()
|
|
|
|
|
2023-05-11 18:39:49 +00:00
|
|
|
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : ""
|
|
|
|
let sensitive = !contentWarning.isEmpty
|
2023-04-16 17:23:13 +00:00
|
|
|
|
2023-05-11 13:59:57 +00:00
|
|
|
let request: Request<Status>
|
|
|
|
|
|
|
|
if let editedStatusID = draft.editedStatusID {
|
|
|
|
if mastodonController.instanceFeatures.needsEditAttachmentsInSeparateRequest {
|
|
|
|
await updateEditedAttachments()
|
|
|
|
}
|
|
|
|
|
|
|
|
request = Client.editStatus(
|
|
|
|
id: editedStatusID,
|
|
|
|
text: textForPosting(),
|
|
|
|
contentType: config.contentType,
|
|
|
|
spoilerText: contentWarning,
|
|
|
|
sensitive: sensitive,
|
|
|
|
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
|
|
|
mediaIDs: uploadedAttachments,
|
|
|
|
mediaAttributes: draft.draftAttachments.compactMap {
|
|
|
|
if let id = $0.editedAttachmentID {
|
|
|
|
return EditStatusMediaAttributes(id: id, description: $0.attachmentDescription, focus: nil)
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
},
|
|
|
|
poll: draft.poll.map {
|
|
|
|
EditPollParameters(options: $0.pollOptions.map(\.text), expiresIn: Int($0.duration), multiple: $0.multiple)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
request = Client.createStatus(
|
|
|
|
text: textForPosting(),
|
|
|
|
contentType: config.contentType,
|
|
|
|
inReplyTo: draft.inReplyToID,
|
|
|
|
mediaIDs: uploadedAttachments,
|
|
|
|
sensitive: sensitive,
|
|
|
|
spoilerText: contentWarning,
|
2023-09-26 01:23:28 +00:00
|
|
|
visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue,
|
2023-05-11 13:59:57 +00:00
|
|
|
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
|
|
|
pollOptions: draft.poll?.pollOptions.map(\.text),
|
|
|
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
|
|
|
pollMultiple: draft.poll?.multiple,
|
2023-09-26 01:23:28 +00:00
|
|
|
localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil,
|
2023-07-08 22:24:36 +00:00
|
|
|
idempotencyKey: draft.id.uuidString
|
2023-05-11 13:59:57 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-16 17:23:13 +00:00
|
|
|
do {
|
2023-05-11 13:59:57 +00:00
|
|
|
let (status, _) = try await mastodonController.run(request)
|
2023-04-16 17:23:13 +00:00
|
|
|
currentStep += 1
|
2023-05-11 13:59:57 +00:00
|
|
|
mastodonController.storeCreatedStatus(status)
|
2023-04-16 17:23:13 +00:00
|
|
|
} catch let error as Client.Error {
|
|
|
|
throw Error.posting(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:59:57 +00:00
|
|
|
private func uploadAttachments() async throws -> [String] {
|
|
|
|
// 2 steps (request data, then upload) for each attachment
|
|
|
|
self.totalSteps += 2 * draft.attachments.count
|
|
|
|
|
|
|
|
var attachments: [String] = []
|
2023-04-16 17:23:13 +00:00
|
|
|
attachments.reserveCapacity(draft.attachments.count)
|
2023-04-23 01:16:30 +00:00
|
|
|
for (index, attachment) in draft.draftAttachments.enumerated() {
|
2023-05-11 13:59:57 +00:00
|
|
|
// if this attachment already exists and is being edited, we don't do anything
|
|
|
|
// edits to the description are handled as part of the edit status request
|
|
|
|
if let editedAttachmentID = attachment.editedAttachmentID {
|
|
|
|
attachments.append(editedAttachmentID)
|
|
|
|
currentStep += 2
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-04-16 17:23:13 +00:00
|
|
|
let data: Data
|
|
|
|
let utType: UTType
|
|
|
|
do {
|
|
|
|
(data, utType) = try await getData(for: attachment)
|
|
|
|
currentStep += 1
|
2023-11-02 21:53:26 +00:00
|
|
|
} catch let error as DraftAttachment.ExportError {
|
2023-04-16 17:23:13 +00:00
|
|
|
throw Error.attachmentData(index: index, cause: error)
|
|
|
|
}
|
2023-11-10 19:08:11 +00:00
|
|
|
let uploaded = try await uploadAttachment(index: index, data: data, utType: utType, description: attachment.attachmentDescription)
|
|
|
|
attachments.append(uploaded.id)
|
|
|
|
currentStep += 1
|
2023-04-16 17:23:13 +00:00
|
|
|
}
|
|
|
|
return attachments
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getData(for attachment: DraftAttachment) async throws -> (Data, UTType) {
|
|
|
|
return try await withCheckedThrowingContinuation { continuation in
|
2023-04-23 01:16:30 +00:00
|
|
|
attachment.getData(features: mastodonController.instanceFeatures) { result in
|
2023-04-16 17:23:13 +00:00
|
|
|
switch result {
|
|
|
|
case let .success(res):
|
|
|
|
continuation.resume(returning: res)
|
|
|
|
case let .failure(error):
|
|
|
|
continuation.resume(throwing: error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-10 19:08:11 +00:00
|
|
|
private func uploadAttachment(index: Int, data: Data, utType: UTType, description: String?) async throws -> Attachment {
|
|
|
|
guard let mimeType = utType.preferredMIMEType else {
|
|
|
|
throw Error.attachmentMissingMimeType(index: index, type: utType)
|
|
|
|
}
|
|
|
|
var filename = "file"
|
|
|
|
if let ext = utType.preferredFilenameExtension {
|
|
|
|
filename.append(".\(ext)")
|
|
|
|
}
|
|
|
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: filename)
|
2023-04-16 17:23:13 +00:00
|
|
|
let req = Client.upload(attachment: formAttachment, description: description)
|
2023-11-10 19:08:11 +00:00
|
|
|
do {
|
|
|
|
return try await mastodonController.run(req).0
|
|
|
|
} catch let error as Client.Error {
|
|
|
|
throw Error.attachmentUpload(index: index, cause: error)
|
|
|
|
}
|
2023-04-16 17:23:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:59:57 +00:00
|
|
|
// only needed for akkoma, not used on regular mastodon
|
|
|
|
private func updateEditedAttachments() async {
|
|
|
|
for attachment in draft.draftAttachments {
|
|
|
|
guard let id = attachment.editedAttachmentID else {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
let req = Client.updateAttachment(id: id, description: attachment.attachmentDescription, focus: nil)
|
|
|
|
_ = try? await mastodonController.run(req)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-16 17:23:13 +00:00
|
|
|
enum Error: Swift.Error, LocalizedError {
|
2023-11-02 21:53:26 +00:00
|
|
|
case attachmentData(index: Int, cause: DraftAttachment.ExportError)
|
2023-11-10 19:08:11 +00:00
|
|
|
case attachmentMissingMimeType(index: Int, type: UTType)
|
2023-04-16 17:23:13 +00:00
|
|
|
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)"
|
2023-11-10 19:08:11 +00:00
|
|
|
case let .attachmentMissingMimeType(index: index, type: type):
|
|
|
|
return "Attachment \(index + 1): unknown MIME type for \(type.identifier)"
|
2023-04-16 17:23:13 +00:00
|
|
|
case let .attachmentUpload(index: index, cause: cause):
|
|
|
|
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
|
|
|
case let .posting(error):
|
|
|
|
return error.localizedDescription
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|