import Foundation /// Parameters for creating a new post. public struct NewPostOptions: Sendable { public enum Visibility: String, Sendable { case `public` case mention } public var text: String public var lang: String? public var tags: String? /// Full post URL to reply to: `"https://feed.url/social.org#TIMESTAMP"`. public var replyTo: String? /// Full post URL to boost: `"https://feed.url/social.org#TIMESTAMP"`. public var include: String? /// Emoji or short text reaction. Combined with `replyTo` and empty `text` = reaction post. public var mood: String? /// Group in `"Name https://relay.url"` format. public var group: String? public var visibility: Visibility? public var client: String /// Poll close date. Body must contain `- [ ] Option` lines. Written as `:POLL_END:`. public var pollEnd: Date? /// The option text chosen when voting on a poll. Combined with `replyTo`. Written as `:POLL_OPTION:`. public var pollOption: String? /// Account migration in `"old-url new-url"` format. Written as `:MIGRATION:`. public var migration: String? public init( text: String, lang: String? = nil, tags: String? = nil, replyTo: String? = nil, include: String? = nil, mood: String? = nil, group: String? = nil, visibility: Visibility? = nil, client: String = "iOS", pollEnd: Date? = nil, pollOption: String? = nil, migration: String? = nil ) { self.text = text self.lang = lang self.tags = tags self.replyTo = replyTo self.include = include self.mood = mood self.group = group self.visibility = visibility self.client = client self.pollEnd = pollEnd self.pollOption = pollOption self.migration = migration } // MARK: - Factories /// Reaction post: `REPLY_TO` + `MOOD`, no body text. public static func reaction(to postURL: String, mood: String) -> NewPostOptions { NewPostOptions(text: "", replyTo: postURL, mood: mood) } /// Boost post: `INCLUDE` pointing to the boosted post URL. /// `commentary` is optional body text (quote-boost). public static func boost(of postURL: String, commentary: String = "") -> NewPostOptions { NewPostOptions(text: commentary, include: postURL) } /// Poll post: checkboxes in body, `POLL_END` property. /// /// - Parameters: /// - question: Optional introductory text above the options. /// - options: Non-empty list of poll option strings. /// - end: Poll close date. /// - lang: Optional `:LANG:`. /// - tags: Optional `:TAGS:`. /// - mood: Optional `:MOOD:`. /// - visibility: Optional `:VISIBILITY:` (`.public` is implicit). public static func poll( question: String = "", options: [String], end: Date, lang: String? = nil, tags: String? = nil, mood: String? = nil, visibility: Visibility? = nil ) -> NewPostOptions { let checkboxes = options.map { "- [ ] \($0)" }.joined(separator: "\n") // Org Mode requires a blank line between a paragraph and a list // for the list to be recognised; without it Emacs renders the // checkboxes as a single wrapped line. let body = question.isEmpty ? checkboxes : "\(question)\n\n\(checkboxes)" return NewPostOptions( text: body, lang: lang, tags: tags, mood: mood, visibility: visibility, pollEnd: end ) } /// Poll vote: `REPLY_TO` + `POLL_OPTION`. /// /// - Parameters: /// - option: The exact option text being voted for. /// - pollURL: Canonical URL of the poll post (`feed#timestamp`). public static func vote(for option: String, on pollURL: String) -> NewPostOptions { NewPostOptions(text: "", replyTo: pollURL, pollOption: option) } /// Account migration post: `MIGRATION: old-url new-url`. public static func migration(from oldURL: String, to newURL: String) -> NewPostOptions { NewPostOptions(text: "", migration: "\(oldURL) \(newURL)") } }