Tusker/Tusker/API/MastodonController.swift

395 lines
14 KiB
Swift
Raw Normal View History

2018-08-16 11:46:19 +00:00
//
// MastodonController.swift
// Tusker
//
// Created by Shadowfacts on 8/15/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
2018-09-11 14:52:21 +00:00
import Pachyderm
2022-11-30 01:52:39 +00:00
import Combine
2018-08-16 11:46:19 +00:00
class MastodonController: ObservableObject {
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
@available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
2018-08-16 11:46:19 +00:00
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
}
}
2018-08-16 22:55:40 +00:00
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!
2018-08-16 11:46:19 +00:00
@Published private(set) var account: Account!
@Published private(set) var instance: Instance!
@Published private(set) var nodeInfo: NodeInfo!
@Published private(set) var instanceFeatures = InstanceFeatures()
2022-11-19 19:08:39 +00:00
@Published private(set) var lists: [List] = []
2022-11-30 01:52:39 +00:00
@Published private(set) var customEmojis: [Emoji]?
@Published private(set) var followedHashtags: [FollowedHashtag] = []
2022-12-01 03:16:33 +00:00
@Published private(set) var filters: [FilterMO] = []
2022-11-30 01:52:39 +00:00
private var cancellables = Set<AnyCancellable>()
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
2021-04-04 19:11:29 +00:00
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
2022-11-30 01:52:39 +00:00
$instance
.combineLatest($nodeInfo)
.compactMap { (instance, nodeInfo) in
if let instance {
return (instance, nodeInfo)
} else {
return nil
}
}
.sink { [unowned self] (instance, nodeInfo) in
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
}
.store(in: &cancellables)
$instanceFeatures
.filter { [unowned self] in $0.canFollowHashtags && self.followedHashtags.isEmpty }
.sink { [unowned self] _ in
Task {
await self.loadFollowedHashtags()
}
}
.store(in: &cancellables)
2018-08-16 22:55:40 +00:00
}
@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?) {
let result: (Result, Pagination?) = 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))
}
}
})
try Task.checkCancellation()
return result
}
/// - 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)
2018-08-16 22:55:40 +00:00
}
2018-08-17 00:11:56 +00:00
}
/// - 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)
}
}
})
2018-08-16 11:46:19 +00:00
}
2022-12-04 03:16:43 +00:00
@MainActor
func initialize() {
// we want this to happen immediately, and synchronously so that the filters (which don't change that often)
// are available when Filterers are constructed
loadCachedFilters()
2022-11-19 19:08:39 +00:00
2022-12-04 03:16:43 +00:00
Task {
do {
async let ownAccount = try getOwnAccount()
async let ownInstance = try getOwnInstance()
_ = try await (ownAccount, ownInstance)
loadLists()
async let _ = await loadFilters()
} catch {
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
}
}
2022-11-19 19:08:39 +00:00
}
func getOwnAccount(completion: ((Result<Account, Client.Error>) -> Void)? = nil) {
2018-10-02 23:23:50 +00:00
if account != nil {
completion?(.success(account))
2018-10-02 23:23:50 +00:00
} 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)
}
completion?(.success(account))
}
}
2018-10-02 23:23:50 +00:00
}
2018-08-31 02:30:19 +00:00
}
}
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) {
if case let .success(instance) = $0 {
completion?(instance)
}
}
2021-04-04 19:11:29 +00:00
}
@MainActor
func getOwnInstance() async throws -> Instance {
return try await withCheckedThrowingContinuation({ continuation in
getOwnInstanceInternal(retryAttempt: 0) { result in
continuation.resume(with: result)
}
})
}
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
2021-04-04 19:11:29 +00:00
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread)
if let instance = self.instance {
completion?(.success(instance))
} else {
2021-04-04 19:11:29 +00:00
if let completion = completion {
pendingOwnInstanceRequestCallbacks.append(completion)
}
if ownInstanceRequest == nil {
let request = Client.getInstance()
ownInstanceRequest = run(request) { (response) in
switch response {
case .failure(let error):
2021-04-04 19:11:29 +00:00
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 :/
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(.failure(error))
}
self.pendingOwnInstanceRequestCallbacks = []
2021-04-04 19:11:29 +00:00
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(.success(instance))
2021-04-04 19:11:29 +00:00
}
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
}
}
}
}
2018-09-30 02:20:17 +00:00
}
}
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([])
}
}
}
}
2022-11-19 19:08:39 +00:00
private func loadLists() {
let req = Client.getLists()
run(req) { response in
if case .success(let lists, _) = response {
DispatchQueue.main.async {
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
2022-11-19 19:08:39 +00:00
}
}
}
}
@MainActor
func addedList(_ list: List) {
var new = self.lists
new.append(list)
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
2022-11-19 19:08:39 +00:00
self.lists = new
}
@MainActor
func deletedList(_ list: List) {
self.lists.removeAll(where: { $0.id == list.id })
}
@MainActor
func renamedList(_ list: List) {
var new = self.lists
if let index = new.firstIndex(where: { $0.id == list.id }) {
new[index] = list
}
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
2022-11-19 19:08:39 +00:00
self.lists = new
}
@MainActor
private func loadFollowedHashtags() async {
2022-11-30 03:52:31 +00:00
updateFollowedHashtags()
let req = Client.getFollowedHashtags()
if let (hashtags, _) = try? await run(req) {
self.persistentContainer.updateFollowedHashtags(hashtags) {
if case .success(let hashtags) = $0 {
self.followedHashtags = hashtags
}
}
}
}
2022-11-30 03:52:31 +00:00
@MainActor
func updateFollowedHashtags() {
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
}
2022-12-01 03:16:33 +00:00
@MainActor
func loadFilters() async {
var apiFilters: [AnyFilter]?
if instanceFeatures.filtersV2 {
let req = Client.getFiltersV2()
if let (filters, _) = try? await run(req) {
apiFilters = filters.map { .v2($0) }
}
} else {
let req = Client.getFiltersV1()
if let (filters, _) = try? await run(req) {
apiFilters = filters.map { .v1($0) }
}
}
if let apiFilters {
self.persistentContainer.updateFilters(apiFilters) {
2022-12-01 03:16:33 +00:00
if case .success(let filters) = $0 {
self.filters = filters
}
}
}
}
2022-12-04 03:16:43 +00:00
@MainActor
private func loadCachedFilters() {
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
}
2022-11-19 19:08:39 +00:00
}