// // MastodonController.swift // Tusker // // Created by Shadowfacts on 8/15/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import Foundation import Pachyderm class MastodonController: ObservableObject { static private(set) var all = [LocalData.UserAccountInfo: MastodonController]() @available(*, message: "do something less dumb") static var first: MastodonController { all.first!.value } static func getForAccount(_ account: LocalData.UserAccountInfo) -> MastodonController { if let controller = all[account] { return controller } else { let controller = MastodonController(instanceURL: account.instanceURL) controller.accountInfo = account controller.client.clientID = account.clientID controller.client.clientSecret = account.clientSecret controller.client.accessToken = account.accessToken all[account] = controller return controller } } static func resetAll() { all = [:] } private let transient: Bool private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) let instanceURL: URL var accountInfo: LocalData.UserAccountInfo? let client: Client! @Published private(set) var account: Account! @Published private(set) var instance: Instance! private(set) var customEmojis: [Emoji]? @MainActor private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]() @MainActor private var ownInstanceRequest: URLSessionTask? var loggedIn: Bool { accountInfo != nil } init(instanceURL: URL, transient: Bool = false) { self.instanceURL = instanceURL self.accountInfo = nil self.client = Client(baseURL: instanceURL) self.transient = transient } @discardableResult func run(_ request: Request, completion: @escaping Client.Callback) -> URLSessionTask? { return client.run(request, completion: completion) } @discardableResult func run(_ request: Request) async throws -> (Result, Pagination?) { return try await client.run(request) } func registerApp() async -> (String, String) { guard client.clientID == nil, client.clientSecret == nil else { return (client.clientID!, client.clientSecret!) } let app = try! await client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) return (app.clientID, app.clientSecret) } func authorize(authorizationCode: String) async -> String { return try! await client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth").accessToken } func getOwnAccount(completion: ((Result) -> Void)? = nil) { if account != nil { completion?(.success(account)) } else { let request = Client.getSelfAccount() run(request) { response in switch response { case let .failure(error): completion?(.failure(error)) case let .success(account, _): DispatchQueue.main.async { self.account = account } self.persistentContainer.backgroundContext.perform { if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) { accountMO.updateFrom(apiAccount: account, container: self.persistentContainer) } else { // the first time the user's account is added to the store, // increment its reference count so that it's never removed self.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) } completion?(.success(account)) } } } } } func getOwnAccount() async throws -> Account { return try await withCheckedThrowingContinuation { continuation in getOwnAccount(completion: continuation.resume) } } @MainActor func getOwnInstance(completion: ((Instance) -> Void)? = nil) { getOwnInstanceInternal(retryAttempt: 0, completion: completion) } @MainActor private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) { if let instance = self.instance { completion?(instance) } else { if let completion = completion { pendingOwnInstanceRequestCallbacks.append(completion) } if ownInstanceRequest == nil { let request = Client.getInstance() ownInstanceRequest = run(request) { (response) in switch response { case .failure(_): let delay: DispatchTimeInterval switch retryAttempt { case 0: delay = .seconds(1) case 1: delay = .seconds(5) case 2: delay = .seconds(30) case 3: delay = .seconds(60) default: // if we've failed four times, just give up :/ return } DispatchQueue.main.asyncAfter(deadline: .now() + delay) { // completion is nil because in this invocation of getOwnInstanceInternal we've already added it to the pending callbacks array self.getOwnInstanceInternal(retryAttempt: retryAttempt + 1, completion: nil) } case let .success(instance, _): DispatchQueue.main.async { self.ownInstanceRequest = nil self.instance = instance for completion in self.pendingOwnInstanceRequestCallbacks { completion(instance) } self.pendingOwnInstanceRequestCallbacks = [] } } } } } } func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) { if let emojis = self.customEmojis { completion(emojis) } else { let request = Client.getCustomEmoji() run(request) { (response) in if case let .success(emojis, _) = response { self.customEmojis = emojis completion(emojis) } else { completion([]) } } } } }