A WIP iOS app for Mastodon and Pleroma.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

XCBActions.swift 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. //
  2. // XCBActions.swift
  3. // Tusker
  4. //
  5. // Created by Shadowfacts on 9/23/18.
  6. // Copyright © 2018 Shadowfacts. All rights reserved.
  7. //
  8. import UIKit
  9. import Pachyderm
  10. import SwiftSoup
  11. struct XCBActions {
  12. // MARK: - Utils
  13. static func presentModally(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) {
  14. UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: animated, completion: completion)
  15. }
  16. static func presentNav(_ vc: UIViewController, animated: Bool) {
  17. let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController
  18. let navController = tabBarController.selectedViewController as! UINavigationController
  19. navController.pushViewController(vc, animated: animated)
  20. }
  21. static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) {
  22. if let id = request.arguments["statusID"] {
  23. MastodonCache.status(for: id) { (status) in
  24. if let status = status {
  25. completion(status)
  26. } else {
  27. session.complete(with: .error, additionalData: [
  28. "error": "Could not get status with ID \(id)"
  29. ])
  30. }
  31. }
  32. } else if let searchQuery = request.arguments["statusURL"] {
  33. let request = MastodonController.shared.client.search(query: searchQuery)
  34. MastodonController.shared.client.run(request) { (response) in
  35. if case let .success(results, _) = response,
  36. let status = results.statuses.first {
  37. MastodonCache.add(status: status)
  38. completion(status)
  39. } else {
  40. session.complete(with: .error, additionalData: [
  41. "error": "Could not find status by searching '\(searchQuery)'"
  42. ])
  43. }
  44. }
  45. } else {
  46. session.complete(with: .error, additionalData: [
  47. "error": "No status provided. Specify either instance-local statusID or remote statusURL."
  48. ])
  49. }
  50. }
  51. static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) {
  52. if let id = request.arguments["accountID"] {
  53. MastodonCache.account(for: id) { (account) in
  54. if let account = account {
  55. completion(account)
  56. } else {
  57. session.complete(with: .error, additionalData: [
  58. "error": "Could not get account with ID \(id)"
  59. ])
  60. }
  61. }
  62. } else if let searchQuery = request.arguments["accountURL"] {
  63. let request = MastodonController.shared.client.search(query: searchQuery)
  64. MastodonController.shared.client.run(request) { (response) in
  65. if case let .success(results, _) = response {
  66. if let account = results.accounts.first {
  67. MastodonCache.add(account: account)
  68. completion(account)
  69. } else {
  70. session.complete(with: .error, additionalData: [
  71. "error": "Could not find account by searching '\(searchQuery)'"
  72. ])
  73. }
  74. } else if case let .failure(error) = response {
  75. session.complete(with: .error, additionalData: [
  76. "error": error.localizedDescription
  77. ])
  78. }
  79. }
  80. } else if let acct = request.arguments["acct"] {
  81. let request = MastodonController.shared.client.searchForAccount(query: acct)
  82. MastodonController.shared.client.run(request) { (response) in
  83. if case let .success(accounts, _) = response {
  84. if let account = accounts.first {
  85. MastodonCache.add(account: account)
  86. completion(account)
  87. } else {
  88. session.complete(with: .error, additionalData: [
  89. "error": "Could not find account \(acct)"
  90. ])
  91. }
  92. } else if case let .failure(error) = response {
  93. session.complete(with: .error, additionalData: [
  94. "error": error.localizedDescription
  95. ])
  96. }
  97. }
  98. } else {
  99. session.complete(with: .error, additionalData: [
  100. "error": "No status provided. Specify either instance-local ID, account URL, or qualified username."
  101. ])
  102. }
  103. }
  104. // MARK: - Statuses
  105. static func showStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  106. getStatus(from: request, session: session) { (status) in
  107. let vc = ConversationViewController.create(for: status.id)
  108. DispatchQueue.main.async {
  109. presentNav(vc, animated: true)
  110. }
  111. }
  112. }
  113. static func postStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  114. let mentioning = request.arguments["mentioning"]
  115. let text = request.arguments["text"]
  116. if silent ?? false {
  117. var status = ""
  118. if let mentioning = mentioning { status += mentioning }
  119. if let text = text { status += text }
  120. guard CharacterCounter.count(text: status) <= MastodonController.shared.instance.maxStatusCharacters ?? 500 else {
  121. session.complete(with: .error, additionalData: [
  122. "error": "Too many characters. Instance maximum is \(MastodonController.shared.instance.maxStatusCharacters ?? 500)"
  123. ])
  124. return
  125. }
  126. let request = MastodonController.shared.client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility)
  127. MastodonController.shared.client.run(request) { response in
  128. if case let .success(status, _) = response {
  129. session.complete(with: .success, additionalData: [
  130. "statusURL": status.url?.absoluteString,
  131. "statusURI": status.uri
  132. ])
  133. } else if case let .failure(error) = response {
  134. session.complete(with: .error, additionalData: [
  135. "error": error.localizedDescription
  136. ])
  137. }
  138. }
  139. } else {
  140. let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text)
  141. presentModally(vc, animated: true)
  142. }
  143. }
  144. static func getStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  145. getStatus(from: request, session: session) { (status) in
  146. let html = Bool(request.arguments["html"] ?? "false") ?? false
  147. let content: String
  148. if html {
  149. content = status.content
  150. } else {
  151. do {
  152. let doc = try SwiftSoup.parse(status.content)
  153. content = try doc.body()!.text()
  154. } catch {
  155. session.complete(with: .error, additionalData: [
  156. "error": error.localizedDescription
  157. ])
  158. return
  159. }
  160. }
  161. session.complete(with: .success, additionalData: [
  162. "url": status.url?.absoluteString,
  163. "uri": status.uri,
  164. "id": status.id,
  165. "account": status.account.acct,
  166. "inReplyTo": status.inReplyToID,
  167. "posted": status.createdAt.timeIntervalSince1970.description,
  168. "content": content,
  169. "reblog": status.reblog?.id
  170. ])
  171. }
  172. }
  173. static func favoriteStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  174. statusAction(request: Status.favourite, alertTitle: "Favorite status?", request, session, silent)
  175. }
  176. static func reblogStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  177. statusAction(request: Status.reblog, alertTitle: "Reblog status?", request, session, silent)
  178. }
  179. static func statusAction(request: @escaping (Status) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  180. func performAction(status: Status, completion: ((Status) -> Void)?) {
  181. MastodonController.shared.client.run(request(status)) { (response) in
  182. if case let .success(status, _) = response {
  183. MastodonCache.add(status: status)
  184. completion?(status)
  185. session.complete(with: .success, additionalData: [
  186. "statusURL": status.url?.absoluteString,
  187. "statusURI": status.uri
  188. ])
  189. } else if case let .failure(error) = response {
  190. session.complete(with: .error, additionalData: [
  191. "error": error.localizedDescription
  192. ])
  193. }
  194. }
  195. }
  196. func favorite(_ status: Status) {
  197. if silent ?? false {
  198. performAction(status: status, completion: nil)
  199. } else {
  200. let vc = ConversationViewController.create(for: status.id)
  201. DispatchQueue.main.async {
  202. presentNav(vc, animated: true)
  203. }
  204. let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert)
  205. alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
  206. performAction(status: status, completion: { (status) in
  207. DispatchQueue.main.async {
  208. vc.tableView.reloadData()
  209. }
  210. })
  211. }))
  212. alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
  213. session.complete(with: .cancel)
  214. }))
  215. DispatchQueue.main.async {
  216. presentModally(alertController, animated: true)
  217. }
  218. }
  219. }
  220. getStatus(from: url, session: session, completion: favorite)
  221. }
  222. // MARK: - Accounts
  223. static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  224. getAccount(from: request, session: session) { (account) in
  225. let vc = ProfileTableViewController.create(for: account.id)
  226. DispatchQueue.main.async {
  227. presentNav(vc, animated: true)
  228. }
  229. }
  230. }
  231. static func getAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  232. getAccount(from: request, session: session) { (account) in
  233. session.complete(with: .success, additionalData: [
  234. "username": account.acct,
  235. "displayName": account.displayName,
  236. "locked": account.locked.description,
  237. "followers": account.followersCount.description,
  238. "following": account.followingCount.description,
  239. "url": account.url.absoluteString,
  240. "avatarURL": account.avatar.absoluteString,
  241. "headerURL": account.header.absoluteString
  242. ])
  243. }
  244. }
  245. static func getCurrentUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  246. let account = MastodonController.shared.account!
  247. session.complete(with: .success, additionalData: [
  248. "username": account.acct,
  249. "displayName": account.displayName,
  250. "locked": account.locked.description,
  251. "followers": account.followersCount.description,
  252. "following": account.followingCount.description,
  253. "url": account.url.absoluteString,
  254. "avatarURL": account.avatar.absoluteString,
  255. "headerURL": account.header.absoluteString
  256. ])
  257. }
  258. static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
  259. func performAction(_ account: Account) {
  260. let request = Account.follow(account.id)
  261. MastodonController.shared.client.run(request) { (response) in
  262. if case let .success(relationship, _) = response {
  263. MastodonCache.add(relationship: relationship)
  264. session.complete(with: .success, additionalData: [
  265. "url": account.url.absoluteString
  266. ])
  267. } else if case let .failure(error) = response {
  268. session.complete(with: .error, additionalData: [
  269. "error": error.localizedDescription
  270. ])
  271. }
  272. }
  273. }
  274. func follow(_ account: Account) {
  275. if silent ?? false {
  276. performAction(account)
  277. } else {
  278. let vc = ProfileTableViewController.create(for: account.id)
  279. DispatchQueue.main.async {
  280. presentNav(vc, animated: true)
  281. }
  282. let alertController = UIAlertController(title: "Follow \(account.realDisplayName)?", message: nil, preferredStyle: .alert)
  283. alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
  284. performAction(account)
  285. }))
  286. alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
  287. session.complete(with: .cancel)
  288. }))
  289. DispatchQueue.main.async {
  290. presentModally(alertController, animated: true)
  291. }
  292. }
  293. }
  294. getAccount(from: request, session: session, completion: follow)
  295. }
  296. }