forked from shadowfacts/Tusker
249 lines
9.7 KiB
Swift
249 lines
9.7 KiB
Swift
//
|
|
// 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!
|
|
@Published private(set) var nodeInfo: NodeInfo!
|
|
@Published private(set) var instanceFeatures = InstanceFeatures()
|
|
private(set) var customEmojis: [Emoji]?
|
|
|
|
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
|
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, session: .appDefault)
|
|
self.transient = transient
|
|
}
|
|
|
|
@discardableResult
|
|
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) -> URLSessionTask? {
|
|
return client.run(request, completion: completion)
|
|
}
|
|
|
|
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
|
return try await withCheckedThrowingContinuation({ continuation in
|
|
client.run(request) { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
continuation.resume(throwing: error)
|
|
case .success(let result, let pagination):
|
|
continuation.resume(returning: (result, pagination))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// - Returns: A tuple of client ID and client secret.
|
|
func registerApp() async throws -> (String, String) {
|
|
if let clientID = client.clientID,
|
|
let clientSecret = client.clientSecret {
|
|
return (clientID, clientSecret)
|
|
} else {
|
|
let app: RegisteredApplication = try await withCheckedThrowingContinuation({ continuation in
|
|
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
continuation.resume(throwing: error)
|
|
case .success(let app, _):
|
|
continuation.resume(returning: app)
|
|
}
|
|
}
|
|
})
|
|
self.client.clientID = app.clientID
|
|
self.client.clientSecret = app.clientSecret
|
|
return (app.clientID, app.clientSecret)
|
|
}
|
|
}
|
|
|
|
/// - Returns: The access token
|
|
func authorize(authorizationCode: String) async throws -> String {
|
|
return try await withCheckedThrowingContinuation({ continuation in
|
|
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
continuation.resume(throwing: error)
|
|
case .success(let settings, _):
|
|
self.client.accessToken = settings.accessToken
|
|
continuation.resume(returning: settings.accessToken)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func getOwnAccount(completion: ((Result<Account, Client.Error>) -> 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 {
|
|
if let account = account {
|
|
return account
|
|
} else {
|
|
return try await withCheckedThrowingContinuation({ continuation in
|
|
self.getOwnAccount { result in
|
|
continuation.resume(with: result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
|
getOwnInstanceInternal(retryAttempt: 0, completion: completion)
|
|
}
|
|
|
|
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
|
|
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
|
assert(Thread.isMainThread)
|
|
|
|
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
|
|
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
|
|
|
|
for completion in self.pendingOwnInstanceRequestCallbacks {
|
|
completion(instance)
|
|
}
|
|
self.pendingOwnInstanceRequestCallbacks = []
|
|
}
|
|
}
|
|
}
|
|
|
|
client.nodeInfo { result in
|
|
switch result {
|
|
case let .failure(error):
|
|
print("Unable to get node info: \(error)")
|
|
|
|
case let .success(nodeInfo, _):
|
|
DispatchQueue.main.async {
|
|
self.nodeInfo = nodeInfo
|
|
if let instance = self.instance {
|
|
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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([])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|