// // MastodonDataManager.swift // elpha-ios // // Created by Dwayne Harris on 10/1/18. // Copyright © 2018 Elpha. All rights reserved. // import CoreData import Foundation class UpsertResult { var object: T var new: Bool = false init(object: T, new: Bool) { self.object = object self.new = new } } @objc public class AccountField: NSObject, NSCoding { let name: String let value: String init(name: String, value: String) { self.name = name self.value = value } public func encode(with aCoder: NSCoder) { aCoder.encode(name, forKey: "name") aCoder.encode(value, forKey: "value") } convenience required public init?(coder aDecoder: NSCoder) { guard let name = aDecoder.decodeObject(forKey: "name") as? String, let value = aDecoder.decodeObject(forKey: "value") as? String else { return nil } self.init(name: name, value: value) } } @objc public class Mention: NSObject, NSCoding { let id: String let url: URL let username: String let acct: String init(id: String, url: URL, username: String, acct: String) { self.id = id self.url = url self.username = username self.acct = acct } public func encode(with aCoder: NSCoder) { aCoder.encode(id, forKey: "id") aCoder.encode(url, forKey: "url") aCoder.encode(username, forKey: "username") aCoder.encode(acct, forKey: "acct") } convenience required public init?(coder aDecoder: NSCoder) { guard let id = aDecoder.decodeObject(forKey: "id") as? String, let url = aDecoder.decodeObject(forKey: "url") as? URL, let username = aDecoder.decodeObject(forKey: "username") as? String, let acct = aDecoder.decodeObject(forKey: "acct") as? String else { return nil } self.init(id: id, url: url, username: username, acct: acct) } } @objc public class Emoji: NSObject, NSCoding { let shortcode: String let staticURL: URL let url: URL let visibleInPicker: Bool init(shortcode: String, staticURL: URL, url: URL, visibleInPicker: Bool) { self.shortcode = shortcode self.staticURL = staticURL self.url = url self.visibleInPicker = visibleInPicker } public func encode(with aCoder: NSCoder) { aCoder.encode(shortcode, forKey: "shortcode") aCoder.encode(staticURL, forKey: "staticURL") aCoder.encode(url, forKey: "url") aCoder.encode(visibleInPicker, forKey: "visibleInPicker") } convenience required public init?(coder aDecoder: NSCoder) { guard let shortcode = aDecoder.decodeObject(forKey: "shortcode") as? String, let staticURL = aDecoder.decodeObject(forKey: "staticURL") as? URL, let url = aDecoder.decodeObject(forKey: "url") as? URL else { return nil } self.init(shortcode: shortcode, staticURL: staticURL, url: url, visibleInPicker: aDecoder.decodeBool(forKey: "acct")) } } @objc public class PaginationMarker: NSObject, NSCoding { let context: String let item: PaginationItem init(context: String, item: PaginationItem) { self.context = context self.item = item } public func encode(with aCoder: NSCoder) { aCoder.encode(context, forKey: "context") aCoder.encode(item, forKey: "item") } convenience required public init?(coder aDecoder: NSCoder) { guard let context = aDecoder.decodeObject(forKey: "context") as? String, let item = aDecoder.decodeObject(forKey: "item") as? PaginationItem else { return nil } self.init(context: context, item: item) } } public class MastodonDataManager { static var dateFormatter: DateFormatter { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" dateFormatter.timeZone = TimeZone(abbreviation: "UTC") dateFormatter.locale = Locale(identifier: "en_US_POSIX") return dateFormatter } static func setAccount(_ account: AccountMO, withData data: JSONObject) -> AccountMO { let dateFormatter = self.dateFormatter account.fetchedAt = Date() account.id = data["id"] as? String account.username = data["username"] as? String account.acct = data["acct"] as? String account.displayName = data["display_name"] as? String account.note = data["note"] as? String account.url = URL(string: data["url"] as! String) account.avatarURL = URL(string: data["avatar"] as! String) account.avatarStaticURL = URL(string: data["avatar_static"] as! String) account.headerURL = URL(string: data["header"] as! String) account.headerStaticURL = URL(string: data["header_static"] as! String) account.locked = data["locked"] as? Bool ?? false account.createdAt = dateFormatter.date(from: data["created_at"] as! String) account.followersCount = data["followers_count"] as! Int32 account.followingCount = data["following_count"] as! Int32 account.statusesCount = data["statuses_count"] as! Int32 if let fields = data["fields"] as? [[String: Any]] { account.fields = fields.map { AccountField(name: $0["name"] as! String, value: $0["value"] as! String) } } return account } static func upsertAccount(_ data: JSONObject) -> AccountMO? { let request = NSFetchRequest(entityName: "Account") request.predicate = NSPredicate(format: "id == %@", data["id"] as! String) do { let results = try CoreDataManager.shared.context.fetch(request) if let account = results.first { return setAccount(account, withData: data) } else { return setAccount(AccountMO(context: CoreDataManager.shared.context), withData: data) } } catch { print("\(error)") return nil } } static func setAttachment(_ attachment: AttachmentMO, withData data: JSONObject) -> AttachmentMO { attachment.id = data["id"] as? String attachment.type = data["type"] as? String attachment.url = URL(string: data["url"] as! String) attachment.previewURL = URL(string: data["preview_url"] as! String) if let remoteURL = data["remote_url"] as? String { attachment.remoteURL = URL(string: remoteURL) } if let textURL = data["text_url"] as? String { attachment.textURL = URL(string: textURL) } attachment.fetchedAt = Date() return attachment } static func upsertAttachment(_ data: JSONObject) -> AttachmentMO? { let request = NSFetchRequest(entityName: "Attachment") request.predicate = NSPredicate(format: "id == %@", data["id"] as! String) do { let results = try CoreDataManager.shared.context.fetch(request) if let attachment = results.first { return setAttachment(attachment, withData: data) } else { return setAttachment(AttachmentMO(context: CoreDataManager.shared.context), withData: data) } } catch { print("\(error)") return nil } } static func setTag(_ tag: TagMO, withData data: JSONObject) -> TagMO { tag.name = data["name"] as? String tag.url = URL(string: data["url"] as! String) return tag } static func upsertTag(_ data: JSONObject) -> TagMO? { let request = NSFetchRequest(entityName: "Tag") request.predicate = NSPredicate(format: "name == %@", data["name"] as! String) do { let results = try CoreDataManager.shared.context.fetch(request) if let tag = results.first { return setTag(tag, withData: data) } else { return setTag(TagMO(context: CoreDataManager.shared.context), withData: data) } } catch { print("\(error)") return nil } } static func setApp(_ app: AppMO, withData data: JSONObject) -> AppMO { app.name = data["name"] as? String if let website = data["website"] as? String { app.website = URL(string: website) } return app } static func upsertApp(_ data: JSONObject) -> AppMO? { let request = NSFetchRequest(entityName: "App") request.predicate = NSPredicate(format: "name == %@", data["name"] as! String) do { let results = try CoreDataManager.shared.context.fetch(request) if let app = results.first { return setApp(app, withData: data) } else { return setApp(AppMO(context: CoreDataManager.shared.context), withData: data) } } catch { print("\(error)") return nil } } static func setStatus(_ status: StatusMO, withData data: JSONObject) -> StatusMO { let dateFormatter = self.dateFormatter status.id = data["id"] as? String status.uri = URL(string: data["uri"] as! String) status.account = MastodonDataManager.upsertAccount(data["account"] as! JSONObject) status.inReplyToID = data["in_reply_to_id"] as? String status.inReplyToAccountID = data["in_reply_to_account_id"] as? String status.content = data["content"] as? String status.createdAt = dateFormatter.date(from: data["created_at"] as! String) status.repliesCount = data["replies_count"] as! Int32 status.reblogsCount = data["reblogs_count"] as! Int32 status.favoritesCount = data["favourites_count"] as! Int32 status.reblogged = data["reblogged"] as? Bool ?? false status.favorited = data["favourited"] as? Bool ?? false status.sensitive = data["sensitive"] as? Bool ?? false status.pinned = data["pinned"] as? Bool ?? false status.spoilerText = data["spoiler_text"] as? String status.visibility = data["visibility"] as? String status.attributedContent = status.content?.htmlAttributed(size: 15.0) status.fetchedAt = Date() if let url = data["url"] as? String { status.url = URL(string: url) } if let app = data["application"] as? JSONObject { status.app = MastodonDataManager.upsertApp(app) } if let attachments = data["media_attachments"] as? [JSONObject] { attachments.forEach { attachment in if let attachment = MastodonDataManager.upsertAttachment(attachment) { status.addToAttachments(attachment) } } } if let mentions = data["mentions"] as? [JSONObject] { status.mentions = mentions.map { mention in return Mention( id: mention["id"] as! String, url: URL(string: mention["url"] as! String)!, username: mention["username"] as! String, acct: mention["acct"] as! String ) } } if let tags = data["tags"] as? [JSONObject] { tags.forEach { tag in if let tag = MastodonDataManager.upsertTag(tag) { status.addToTags(tag) } } } if let emojis = data["emojis"] as? [JSONObject] { status.emojis = emojis.map { emoji in return Emoji( shortcode: emoji["shortcode"] as! String, staticURL: URL(string: emoji["static_url"] as! String)!, url: URL(string: emoji["url"] as! String)!, visibleInPicker: emoji["visible_in_picker"] as? Bool ?? false ) } } if let reblog = data["reblog"] as? JSONObject { let upsertResult = upsertStatus(reblog) status.reblog = upsertResult?.object } return status } static func upsertStatus(_ data: JSONObject) -> UpsertResult? { let request = NSFetchRequest(entityName: "Status") request.predicate = NSPredicate(format: "id == %@", data["id"] as! String) do { let results = try CoreDataManager.shared.context.fetch(request) if let status = results.first { return UpsertResult( object: setStatus(status, withData: data), new: false ) } else { let status = StatusMO(context: CoreDataManager.shared.context) status.hidden = !(data["spoiler_text"] as! String).isEmpty return UpsertResult( object: setStatus(status, withData: data), new: true ) } } catch { print("\(error)") return nil } } static func saveClient(id: String, clientID: String, clientSecret: String, host: String) -> ClientMO { let client = ClientMO(context: CoreDataManager.shared.context) client.id = id client.clientID = clientID client.clientSecret = clientSecret client.host = host CoreDataManager.shared.saveContext() return client } static func account(id: String) -> AccountMO? { let request = NSFetchRequest(entityName: "Account") request.predicate = NSPredicate(format: "id == %@", id) do { let results = try CoreDataManager.shared.context.fetch(request) guard let account = results.first else { return nil } return account } catch { print("\(error)") return nil } } }