[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.

413 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
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
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
6 years ago
6 years ago
6 years ago
6 years ago
4 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
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
6 years ago
6 years ago
6 years ago
6 years ago
  1. //
  2. // MastodonDataManager.swift
  3. // elpha-ios
  4. //
  5. // Created by Dwayne Harris on 10/1/18.
  6. // Copyright © 2018 Elpha. All rights reserved.
  7. //
  8. import CoreData
  9. import Foundation
  10. class UpsertResult<T: NSManagedObject> {
  11. var object: T
  12. var new: Bool = false
  13. init(object: T, new: Bool) {
  14. self.object = object
  15. self.new = new
  16. }
  17. }
  18. @objc public class AccountField: NSObject, NSCoding {
  19. let name: String
  20. let value: String
  21. init(name: String, value: String) {
  22. self.name = name
  23. self.value = value
  24. }
  25. public func encode(with aCoder: NSCoder) {
  26. aCoder.encode(name, forKey: "name")
  27. aCoder.encode(value, forKey: "value")
  28. }
  29. convenience required public init?(coder aDecoder: NSCoder) {
  30. guard
  31. let name = aDecoder.decodeObject(forKey: "name") as? String,
  32. let value = aDecoder.decodeObject(forKey: "value") as? String
  33. else {
  34. return nil
  35. }
  36. self.init(name: name, value: value)
  37. }
  38. }
  39. @objc public class Mention: NSObject, NSCoding {
  40. let id: String
  41. let url: URL
  42. let username: String
  43. let acct: String
  44. init(id: String, url: URL, username: String, acct: String) {
  45. self.id = id
  46. self.url = url
  47. self.username = username
  48. self.acct = acct
  49. }
  50. public func encode(with aCoder: NSCoder) {
  51. aCoder.encode(id, forKey: "id")
  52. aCoder.encode(url, forKey: "url")
  53. aCoder.encode(username, forKey: "username")
  54. aCoder.encode(acct, forKey: "acct")
  55. }
  56. convenience required public init?(coder aDecoder: NSCoder) {
  57. guard
  58. let id = aDecoder.decodeObject(forKey: "id") as? String,
  59. let url = aDecoder.decodeObject(forKey: "url") as? URL,
  60. let username = aDecoder.decodeObject(forKey: "username") as? String,
  61. let acct = aDecoder.decodeObject(forKey: "acct") as? String
  62. else {
  63. return nil
  64. }
  65. self.init(id: id, url: url, username: username, acct: acct)
  66. }
  67. }
  68. @objc public class Emoji: NSObject, NSCoding {
  69. let shortcode: String
  70. let staticURL: URL
  71. let url: URL
  72. let visibleInPicker: Bool
  73. init(shortcode: String, staticURL: URL, url: URL, visibleInPicker: Bool) {
  74. self.shortcode = shortcode
  75. self.staticURL = staticURL
  76. self.url = url
  77. self.visibleInPicker = visibleInPicker
  78. }
  79. public func encode(with aCoder: NSCoder) {
  80. aCoder.encode(shortcode, forKey: "shortcode")
  81. aCoder.encode(staticURL, forKey: "staticURL")
  82. aCoder.encode(url, forKey: "url")
  83. aCoder.encode(visibleInPicker, forKey: "visibleInPicker")
  84. }
  85. convenience required public init?(coder aDecoder: NSCoder) {
  86. guard
  87. let shortcode = aDecoder.decodeObject(forKey: "shortcode") as? String,
  88. let staticURL = aDecoder.decodeObject(forKey: "staticURL") as? URL,
  89. let url = aDecoder.decodeObject(forKey: "url") as? URL
  90. else {
  91. return nil
  92. }
  93. self.init(shortcode: shortcode, staticURL: staticURL, url: url, visibleInPicker: aDecoder.decodeBool(forKey: "acct"))
  94. }
  95. }
  96. @objc public class PaginationMarker: NSObject, NSCoding {
  97. let context: String
  98. let item: PaginationItem
  99. init(context: String, item: PaginationItem) {
  100. self.context = context
  101. self.item = item
  102. }
  103. public func encode(with aCoder: NSCoder) {
  104. aCoder.encode(context, forKey: "context")
  105. aCoder.encode(item, forKey: "item")
  106. }
  107. convenience required public init?(coder aDecoder: NSCoder) {
  108. guard
  109. let context = aDecoder.decodeObject(forKey: "context") as? String,
  110. let item = aDecoder.decodeObject(forKey: "item") as? PaginationItem
  111. else {
  112. return nil
  113. }
  114. self.init(context: context, item: item)
  115. }
  116. }
  117. public class MastodonDataManager {
  118. static var dateFormatter: DateFormatter {
  119. let dateFormatter = DateFormatter()
  120. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
  121. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  122. dateFormatter.locale = Locale(identifier: "en_US_POSIX")
  123. return dateFormatter
  124. }
  125. static func setAccount(_ account: AccountMO, withData data: JSONObject) -> AccountMO {
  126. let dateFormatter = self.dateFormatter
  127. account.fetchedAt = Date()
  128. account.id = data["id"] as? String
  129. account.username = data["username"] as? String
  130. account.acct = data["acct"] as? String
  131. account.displayName = data["display_name"] as? String
  132. account.note = data["note"] as? String
  133. account.url = URL(string: data["url"] as! String)
  134. account.avatarURL = URL(string: data["avatar"] as! String)
  135. account.avatarStaticURL = URL(string: data["avatar_static"] as! String)
  136. account.headerURL = URL(string: data["header"] as! String)
  137. account.headerStaticURL = URL(string: data["header_static"] as! String)
  138. account.locked = data["locked"] as? Bool ?? false
  139. account.createdAt = dateFormatter.date(from: data["created_at"] as! String)
  140. account.followersCount = data["followers_count"] as! Int32
  141. account.followingCount = data["following_count"] as! Int32
  142. account.statusesCount = data["statuses_count"] as! Int32
  143. if let fields = data["fields"] as? [[String: Any]] {
  144. account.fields = fields.map { AccountField(name: $0["name"] as! String, value: $0["value"] as! String) }
  145. }
  146. return account
  147. }
  148. static func upsertAccount(_ data: JSONObject) -> AccountMO? {
  149. let request = NSFetchRequest<AccountMO>(entityName: "Account")
  150. request.predicate = NSPredicate(format: "id == %@", data["id"] as! String)
  151. do {
  152. let results = try CoreDataManager.shared.context.fetch(request)
  153. if let account = results.first {
  154. return setAccount(account, withData: data)
  155. } else {
  156. return setAccount(AccountMO(context: CoreDataManager.shared.context), withData: data)
  157. }
  158. } catch {
  159. print("\(error)")
  160. return nil
  161. }
  162. }
  163. static func setAttachment(_ attachment: AttachmentMO, withData data: JSONObject) -> AttachmentMO {
  164. attachment.id = data["id"] as? String
  165. attachment.type = data["type"] as? String
  166. attachment.url = URL(string: data["url"] as! String)
  167. attachment.previewURL = URL(string: data["preview_url"] as! String)
  168. if let remoteURL = data["remote_url"] as? String {
  169. attachment.remoteURL = URL(string: remoteURL)
  170. }
  171. if let textURL = data["text_url"] as? String {
  172. attachment.textURL = URL(string: textURL)
  173. }
  174. attachment.fetchedAt = Date()
  175. return attachment
  176. }
  177. static func upsertAttachment(_ data: JSONObject) -> AttachmentMO? {
  178. let request = NSFetchRequest<AttachmentMO>(entityName: "Attachment")
  179. request.predicate = NSPredicate(format: "id == %@", data["id"] as! String)
  180. do {
  181. let results = try CoreDataManager.shared.context.fetch(request)
  182. if let attachment = results.first {
  183. return setAttachment(attachment, withData: data)
  184. } else {
  185. return setAttachment(AttachmentMO(context: CoreDataManager.shared.context), withData: data)
  186. }
  187. } catch {
  188. print("\(error)")
  189. return nil
  190. }
  191. }
  192. static func setTag(_ tag: TagMO, withData data: JSONObject) -> TagMO {
  193. tag.name = data["name"] as? String
  194. tag.url = URL(string: data["url"] as! String)
  195. return tag
  196. }
  197. static func upsertTag(_ data: JSONObject) -> TagMO? {
  198. let request = NSFetchRequest<TagMO>(entityName: "Tag")
  199. request.predicate = NSPredicate(format: "name == %@", data["name"] as! String)
  200. do {
  201. let results = try CoreDataManager.shared.context.fetch(request)
  202. if let tag = results.first {
  203. return setTag(tag, withData: data)
  204. } else {
  205. return setTag(TagMO(context: CoreDataManager.shared.context), withData: data)
  206. }
  207. } catch {
  208. print("\(error)")
  209. return nil
  210. }
  211. }
  212. static func setApp(_ app: AppMO, withData data: JSONObject) -> AppMO {
  213. app.name = data["name"] as? String
  214. if let website = data["website"] as? String {
  215. app.website = URL(string: website)
  216. }
  217. return app
  218. }
  219. static func upsertApp(_ data: JSONObject) -> AppMO? {
  220. let request = NSFetchRequest<AppMO>(entityName: "App")
  221. request.predicate = NSPredicate(format: "name == %@", data["name"] as! String)
  222. do {
  223. let results = try CoreDataManager.shared.context.fetch(request)
  224. if let app = results.first {
  225. return setApp(app, withData: data)
  226. } else {
  227. return setApp(AppMO(context: CoreDataManager.shared.context), withData: data)
  228. }
  229. } catch {
  230. print("\(error)")
  231. return nil
  232. }
  233. }
  234. static func setStatus(_ status: StatusMO, withData data: JSONObject) -> StatusMO {
  235. let dateFormatter = self.dateFormatter
  236. status.id = data["id"] as? String
  237. status.uri = URL(string: data["uri"] as! String)
  238. status.account = MastodonDataManager.upsertAccount(data["account"] as! JSONObject)
  239. status.inReplyToID = data["in_reply_to_id"] as? String
  240. status.inReplyToAccountID = data["in_reply_to_account_id"] as? String
  241. status.content = data["content"] as? String
  242. status.createdAt = dateFormatter.date(from: data["created_at"] as! String)
  243. status.repliesCount = data["replies_count"] as! Int32
  244. status.reblogsCount = data["reblogs_count"] as! Int32
  245. status.favoritesCount = data["favourites_count"] as! Int32
  246. status.reblogged = data["reblogged"] as? Bool ?? false
  247. status.favorited = data["favourited"] as? Bool ?? false
  248. status.sensitive = data["sensitive"] as? Bool ?? false
  249. status.pinned = data["pinned"] as? Bool ?? false
  250. status.spoilerText = data["spoiler_text"] as? String
  251. status.visibility = data["visibility"] as? String
  252. status.attributedContent = status.content?.htmlAttributed(size: 15.0)
  253. status.fetchedAt = Date()
  254. if let url = data["url"] as? String {
  255. status.url = URL(string: url)
  256. }
  257. if let app = data["application"] as? JSONObject {
  258. status.app = MastodonDataManager.upsertApp(app)
  259. }
  260. if let attachments = data["media_attachments"] as? [JSONObject] {
  261. attachments.forEach { attachment in
  262. if let attachment = MastodonDataManager.upsertAttachment(attachment) {
  263. status.addToAttachments(attachment)
  264. }
  265. }
  266. }
  267. if let mentions = data["mentions"] as? [JSONObject] {
  268. status.mentions = mentions.map { mention in
  269. return Mention(
  270. id: mention["id"] as! String,
  271. url: URL(string: mention["url"] as! String)!,
  272. username: mention["username"] as! String,
  273. acct: mention["acct"] as! String
  274. )
  275. }
  276. }
  277. if let tags = data["tags"] as? [JSONObject] {
  278. tags.forEach { tag in
  279. if let tag = MastodonDataManager.upsertTag(tag) {
  280. status.addToTags(tag)
  281. }
  282. }
  283. }
  284. if let emojis = data["emojis"] as? [JSONObject] {
  285. status.emojis = emojis.map { emoji in
  286. return Emoji(
  287. shortcode: emoji["shortcode"] as! String,
  288. staticURL: URL(string: emoji["static_url"] as! String)!,
  289. url: URL(string: emoji["url"] as! String)!,
  290. visibleInPicker: emoji["visible_in_picker"] as? Bool ?? false
  291. )
  292. }
  293. }
  294. if let reblog = data["reblog"] as? JSONObject {
  295. let upsertResult = upsertStatus(reblog)
  296. status.reblog = upsertResult?.object
  297. }
  298. return status
  299. }
  300. static func upsertStatus(_ data: JSONObject) -> UpsertResult<StatusMO>? {
  301. let request = NSFetchRequest<StatusMO>(entityName: "Status")
  302. request.predicate = NSPredicate(format: "id == %@", data["id"] as! String)
  303. do {
  304. let results = try CoreDataManager.shared.context.fetch(request)
  305. if let status = results.first {
  306. return UpsertResult(
  307. object: setStatus(status, withData: data),
  308. new: false
  309. )
  310. } else {
  311. let status = StatusMO(context: CoreDataManager.shared.context)
  312. status.hidden = !(data["spoiler_text"] as! String).isEmpty
  313. return UpsertResult(
  314. object: setStatus(status, withData: data),
  315. new: true
  316. )
  317. }
  318. } catch {
  319. print("\(error)")
  320. return nil
  321. }
  322. }
  323. static func saveClient(id: String, clientID: String, clientSecret: String, host: String) -> ClientMO {
  324. let client = ClientMO(context: CoreDataManager.shared.context)
  325. client.id = id
  326. client.clientID = clientID
  327. client.clientSecret = clientSecret
  328. client.host = host
  329. CoreDataManager.shared.saveContext()
  330. return client
  331. }
  332. static func account(id: String) -> AccountMO? {
  333. let request = NSFetchRequest<AccountMO>(entityName: "Account")
  334. request.predicate = NSPredicate(format: "id == %@", id)
  335. do {
  336. let results = try CoreDataManager.shared.context.fetch(request)
  337. guard let account = results.first else {
  338. return nil
  339. }
  340. return account
  341. } catch {
  342. print("\(error)")
  343. return nil
  344. }
  345. }
  346. }