// // FervorController.swift // Reader // // Created by Shadowfacts on 11/25/21. // import Foundation import Fervor import OSLog import Combine class FervorController { static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")! let instanceURL: URL private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController") let client: FervorClient private(set) var account: LocalData.Account? private(set) var clientID: String? private(set) var clientSecret: String? private(set) var accessToken: String? private(set) var persistentContainer: PersistentContainer! let syncStateSubject = PassthroughSubject() init(instanceURL: URL) { self.instanceURL = instanceURL self.client = FervorClient(instanceURL: instanceURL, accessToken: nil) } convenience init(account: LocalData.Account) { self.init(instanceURL: account.instanceURL) self.account = account self.clientID = account.clientID self.clientSecret = account.clientSecret self.accessToken = account.accessToken self.client.accessToken = account.accessToken self.persistentContainer = PersistentContainer(account: account, fervorController: self) } private func setSyncState(_ state: SyncState) { DispatchQueue.main.async { self.syncStateSubject.send(state) } } func register() async throws -> ClientRegistration { let registration = try await client.register(clientName: "Frenzy iOS", website: nil, redirectURI: FervorController.oauthRedirectURI) clientID = registration.clientID clientSecret = registration.clientSecret return registration } func getToken(authCode: String) async throws { let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) client.accessToken = token.accessToken accessToken = token.accessToken } func syncAll() async throws { setSyncState(.groupsAndFeeds) logger.info("Syncing groups and feeds") async let groups = try client.groups() async let feeds = try client.feeds() try await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds) setSyncState(.items) let lastSync = try await persistentContainer.lastSyncDate() logger.info("Syncing items with last sync date: \(String(describing: lastSync), privacy: .public)") let update = try await client.syncItems(lastSync: lastSync) try await persistentContainer.syncItems(update, setSyncState: setSyncState(_:)) try await persistentContainer.updateLastSyncDate(update.syncTimestamp) setSyncState(.excerpts) await ExcerptGenerator.generateAll(self) setSyncState(.done) } @MainActor func syncReadToServer() async throws { var count = 0 // todo: there should be a batch update api endpoint for case let item as Item in persistentContainer.viewContext.updatedObjects { let f = item.read ? client.read(item:) : client.unread(item:) do { let _ = try await f(item.id!) count += 1 } catch { logger.error("Failed to sync read state: \(String(describing: error), privacy: .public)") item.needsReadStateSync = true } } // try to sync items which failed last time let req = Item.fetchRequest() req.predicate = NSPredicate(format: "needsReadStateSync = YES") if let needsSync = try? persistentContainer.viewContext.fetch(req) { for item in needsSync { let f = item.read ? client.read(item:) : client.unread(item:) do { let _ = try await f(item.id!) count += 1 item.needsReadStateSync = false } catch { logger.error("Failed to sync read state again: \(String(describing: error), privacy: .public)") item.needsReadStateSync = true // todo: this should probably fail after a certain number of attempts } } } logger.info("Synced \(count, privacy: .public) read/unread to server") do { try persistentContainer.viewContext.save() } catch { logger.error("Failed to save view context: \(String(describing: error), privacy: .public)") } } @MainActor func markItem(_ item: Item, read: Bool) async { item.read = read do { let f = item.read ? client.read(item:) : client.unread(item:) _ = try await f(item.id!) item.needsReadStateSync = false } catch { logger.error("Failed to mark item (un)read: \(String(describing: error), privacy: .public)") item.needsReadStateSync = true } if persistentContainer.viewContext.hasChanges { do { try persistentContainer.viewContext.save() } catch { logger.error("Failed to save view context: \(String(describing: error), privacy: .public)") } } } } extension FervorController { enum SyncState: Equatable { case groupsAndFeeds case items case updateItems(current: Int, total: Int) case excerpts case done } }