[ABANDONED] Mastodon iOS client.
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.

447 lines
14 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. //
  2. // MastodonAPI.swift
  3. // elpha-ios
  4. //
  5. // Created by Dwayne Harris on 10/17/18.
  6. // Copyright © 2018 Elpha. All rights reserved.
  7. //
  8. import Alamofire
  9. import Foundation
  10. typealias JSONObject = [String: Any]
  11. typealias JSONArray = [Any]
  12. typealias JSONObjectArray = [JSONObject]
  13. enum MastodonRequestError: Error {
  14. case noResponse, unauthenticated
  15. }
  16. enum TimelineCategory: String {
  17. case home, local, federated, tag, favorites
  18. }
  19. enum PaginationDirection: String {
  20. case prev, next
  21. }
  22. enum AttachmentType: String {
  23. case unknown, image, gifv, video
  24. }
  25. enum StatusVisibility: String, CaseIterable {
  26. case `public`, unlisted, `private`, direct
  27. }
  28. public class PaginationItem: NSObject, NSCoding {
  29. let direction: PaginationDirection
  30. let statusID: String
  31. init(direction: PaginationDirection, statusID: String) {
  32. self.direction = direction
  33. self.statusID = statusID
  34. }
  35. public func encode(with aCoder: NSCoder) {
  36. aCoder.encode(direction.rawValue, forKey: "direction")
  37. aCoder.encode(statusID, forKey: "statusID")
  38. }
  39. convenience required public init?(coder aDecoder: NSCoder) {
  40. guard
  41. let direction = PaginationDirection(rawValue: aDecoder.decodeObject(forKey: "direction") as! String),
  42. let statusID = aDecoder.decodeObject(forKey: "statusID") as? String
  43. else {
  44. return nil
  45. }
  46. self.init(direction: direction, statusID: statusID)
  47. }
  48. }
  49. class MastodonAPI {
  50. static var serverURL: URL? {
  51. guard let host = AuthenticationManager.session?.client?.host else {
  52. return nil
  53. }
  54. return URL(string: "https://\(host)")
  55. }
  56. static func parseLinkHeader(_ link: String?) -> [PaginationItem] {
  57. guard let link = link else {
  58. return []
  59. }
  60. let regex = try! NSRegularExpression(pattern: "<[\\S]+(?:max_id|since_id|min_id)=([0-9]+)[\\S]*>; rel=\"(next|prev)\"", options: .caseInsensitive)
  61. let matches = regex.matches(in: link, options: [], range: NSRange(location: 0, length: link.count))
  62. return matches.map { match in
  63. let statusRange = match.range(at: 1)
  64. let directionRange = match.range(at: 2)
  65. let statusID = link[Range(statusRange, in: link)!]
  66. let direction = link[Range(directionRange, in: link)!]
  67. return PaginationItem(direction: PaginationDirection(rawValue: String(direction))!, statusID: String(statusID))
  68. }
  69. }
  70. private static func request(
  71. token: String,
  72. serverURL: URL,
  73. path: String,
  74. method: HTTPMethod = .get,
  75. parameters: Parameters? = nil,
  76. pagination: PaginationItem? = nil,
  77. completion: @escaping (Any?, [PaginationItem]?, Error?) -> Void
  78. ) {
  79. let requestURL = serverURL.appendingPathComponent(path)
  80. let headers: HTTPHeaders = [
  81. "Authorization": "Bearer \(token)",
  82. "Accept": "application/json",
  83. ]
  84. if Config.logRequests {
  85. print("Request: \(requestURL.absoluteString)")
  86. }
  87. var parameters = parameters ?? [:]
  88. if let pagination = pagination {
  89. switch pagination.direction {
  90. case .prev:
  91. parameters["min_id"] = pagination.statusID
  92. case .next:
  93. parameters["max_id"] = pagination.statusID
  94. }
  95. }
  96. Alamofire.request(
  97. requestURL,
  98. method: method,
  99. parameters: parameters,
  100. encoding: URLEncoding.default,
  101. headers: headers
  102. ).validate().responseJSON { response in
  103. switch response.result {
  104. case .success(let data):
  105. let pagination = self.parseLinkHeader(response.response?.allHeaderFields["Link"] as? String)
  106. completion(data, pagination, nil)
  107. case .failure(let error):
  108. completion(nil, nil, error)
  109. }
  110. }
  111. }
  112. private static func request(
  113. path: String,
  114. method: HTTPMethod = .get,
  115. parameters: Parameters? = nil,
  116. pagination: PaginationItem? = nil,
  117. completion: @escaping (Any?, [PaginationItem]?, Error?) -> Void
  118. ) {
  119. guard let token = AuthenticationManager.token, let serverURL = self.serverURL else {
  120. completion(nil, nil, MastodonRequestError.unauthenticated)
  121. return
  122. }
  123. request(
  124. token: token,
  125. serverURL: serverURL,
  126. path: path,
  127. method: method,
  128. parameters: parameters,
  129. pagination: pagination,
  130. completion: completion
  131. )
  132. }
  133. static func currentUser(token: String, serverURL: URL, completion: @escaping (JSONObject?, Error?) -> Void) {
  134. self.request(token: token, serverURL: serverURL, path: "api/v1/accounts/verify_credentials") { data, _, error in
  135. guard error == nil else {
  136. completion(nil, error)
  137. return
  138. }
  139. completion(data as? JSONObject, nil)
  140. }
  141. }
  142. static func account(id: String, completion: @escaping (JSONObject?, Error?) -> Void) {
  143. self.request(path: "api/v1/accounts/\(id)") { data, _, error in
  144. guard error == nil else {
  145. completion(nil, error)
  146. return
  147. }
  148. completion(data as? JSONObject, nil)
  149. }
  150. }
  151. static func status(id: String, completion: @escaping (JSONObject?, Error?) -> Void) {
  152. self.request(path: "api/v1/statuses/\(id)") { data, _, error in
  153. guard error == nil else {
  154. completion(nil, error)
  155. return
  156. }
  157. completion(data as? JSONObject, nil)
  158. }
  159. }
  160. static func context(id: String, completion: @escaping ([String: Any]?, Error?) -> Void) {
  161. self.request(path: "api/v1/statuses/\(id)/context") { data, _, error in
  162. guard error == nil else {
  163. completion(nil, error)
  164. return
  165. }
  166. completion(data as? [String: Any], nil)
  167. }
  168. }
  169. static func homeTimeline(
  170. limit: Int?,
  171. pagination: PaginationItem?,
  172. completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
  173. ) {
  174. let parameters: Parameters = ["limit": limit ?? 20]
  175. self.request(path: "api/v1/timelines/home", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
  176. guard error == nil else {
  177. completion(nil, nil, error)
  178. return
  179. }
  180. completion(data as? JSONObjectArray, pagination, nil)
  181. }
  182. }
  183. static func publicTimeline(
  184. local: Bool,
  185. limit: Int?,
  186. pagination: PaginationItem?,
  187. completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
  188. ) {
  189. let parameters: Parameters = [
  190. "local": local,
  191. "limit": limit ?? 20,
  192. ]
  193. self.request(path: "api/v1/timelines/public", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
  194. guard error == nil else {
  195. completion(nil, nil, error)
  196. return
  197. }
  198. completion(data as? JSONObjectArray, pagination, nil)
  199. }
  200. }
  201. static func tagTimeline(
  202. tag: String,
  203. local: Bool,
  204. limit: Int?,
  205. pagination: PaginationItem?,
  206. completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
  207. ) {
  208. let parameters: Parameters = [
  209. "local": local,
  210. "limit": limit ?? 20,
  211. ]
  212. self.request(path: "api/v1/timelines/tag/\(tag)", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
  213. guard error == nil else {
  214. completion(nil, nil, error)
  215. return
  216. }
  217. completion(data as? JSONObjectArray, pagination, nil)
  218. }
  219. }
  220. static func statuses(
  221. accountID: String,
  222. onlyMedia: Bool,
  223. excludeReplies: Bool,
  224. limit: Int?,
  225. pagination: PaginationItem?,
  226. completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
  227. ) {
  228. let parameters: Parameters = [
  229. "only_media": onlyMedia,
  230. "exclude_replies": excludeReplies,
  231. "limit": limit ?? 20,
  232. ]
  233. self.request(path: "api/v1/accounts/\(accountID)/statuses", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
  234. guard error == nil else {
  235. completion(nil, nil, error)
  236. return
  237. }
  238. completion(data as? JSONObjectArray, pagination, nil)
  239. }
  240. }
  241. static func pinnedStatuses(
  242. accountID: String,
  243. limit: Int?,
  244. completion: @escaping (JSONObjectArray?, Error?) -> Void
  245. ) {
  246. let parameters: Parameters = [
  247. "only_media": false,
  248. "exclude_replies": false,
  249. "pinned": true,
  250. "limit": limit ?? 20,
  251. ]
  252. self.request(path: "api/v1/accounts/\(accountID)/statuses", method: .get, parameters: parameters) { data, pagination, error in
  253. guard error == nil else {
  254. completion(nil, error)
  255. return
  256. }
  257. completion(data as? JSONObjectArray, nil)
  258. }
  259. }
  260. static func favorites(
  261. limit: Int?,
  262. pagination: PaginationItem?,
  263. completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
  264. ) {
  265. let parameters: Parameters = ["limit": limit ?? 20]
  266. self.request(path: "api/v1/favourites", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
  267. guard error == nil else {
  268. completion(nil, nil, error)
  269. return
  270. }
  271. completion(data as? JSONObjectArray, pagination, nil)
  272. }
  273. }
  274. static func reblog(statusID: String, completion: @escaping (Error?) -> Void) {
  275. self.request(path: "api/v1/statuses/\(statusID)/reblog", method: .post) { _, _, error in
  276. guard error == nil else {
  277. completion(error)
  278. return
  279. }
  280. completion(nil)
  281. }
  282. }
  283. static func unreblog(statusID: String, completion: @escaping (Error?) -> Void) {
  284. self.request(path: "api/v1/statuses/\(statusID)/unreblog", method: .post) { _, _, error in
  285. guard error == nil else {
  286. completion(error)
  287. return
  288. }
  289. completion(nil)
  290. }
  291. }
  292. static func favorite(statusID: String, completion: @escaping (Error?) -> Void) {
  293. self.request(path: "api/v1/statuses/\(statusID)/favourite", method: .post) { _, _, error in
  294. guard error == nil else {
  295. completion(error)
  296. return
  297. }
  298. completion(nil)
  299. }
  300. }
  301. static func unfavorite(statusID: String, completion: @escaping (Error?) -> Void) {
  302. self.request(path: "api/v1/statuses/\(statusID)/unfavourite", method: .post) { _, _, error in
  303. guard error == nil else {
  304. completion(error)
  305. return
  306. }
  307. completion(nil)
  308. }
  309. }
  310. static func postStatus(content: String, replyToID: String?, spoilerText: String?, visibility: StatusVisibility = .public, completion: @escaping (Error?) -> Void) {
  311. var parameters: Parameters = [
  312. "status": content,
  313. "visibility": visibility.rawValue,
  314. ]
  315. if let replyToID = replyToID {
  316. parameters["in_reply_to_id"] = replyToID
  317. }
  318. if let spoilerText = spoilerText {
  319. parameters["spoiler_text"] = spoilerText
  320. }
  321. self.request(path: "api/v1/statuses", method: .post, parameters: parameters) { _, _, error in
  322. guard error == nil else {
  323. completion(error)
  324. return
  325. }
  326. completion(nil)
  327. }
  328. }
  329. static func search(content: String, completion: @escaping (JSONObject?, Error?) -> Void) {
  330. let parameters: Parameters = ["q": content]
  331. self.request(path: "api/v2/search", method: .get, parameters: parameters) { data, _, error in
  332. guard error == nil else {
  333. completion(nil, error)
  334. return
  335. }
  336. completion(data as? JSONObject, nil)
  337. }
  338. }
  339. static func notifications(
  340. limit: Int?,
  341. pagination: PaginationItem?,
  342. completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
  343. ) {
  344. let parameters: Parameters = ["limit": limit ?? 20]
  345. self.request(path: "api/v1/notifications", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
  346. guard error == nil else {
  347. completion(nil, nil, error)
  348. return
  349. }
  350. completion(data as? JSONObjectArray, pagination, nil)
  351. }
  352. }
  353. static func registerApp(serverURL: URL, completion: @escaping (JSONObject?, Error?) -> Void) {
  354. let requestURL = serverURL.appendingPathComponent("api/v1/apps")
  355. let parameters: Parameters = [
  356. "client_name": Config.clientDisplayName,
  357. "redirect_uris": Config.clientRedirectURI,
  358. "scopes": "read write follow",
  359. "website": Config.clientWebsite,
  360. ]
  361. Alamofire.request(
  362. requestURL,
  363. method: .post,
  364. parameters: parameters,
  365. encoding: URLEncoding(destination: .httpBody)
  366. ).validate().responseJSON { response in
  367. switch response.result {
  368. case .success(let data):
  369. completion(data as? JSONObject, nil)
  370. case .failure(let error):
  371. completion(nil, error)
  372. }
  373. }
  374. }
  375. }