// // 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! @Published private(set) var syncState = SyncState.done 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.syncState = 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 { guard syncState == .done else { return } // always return to .done, even if we throw and stop syncing early defer { setSyncState(.done) } 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) } @MainActor func syncReadToServer() async { var count = 0 // try to sync items which failed last time let req = Item.fetchRequest() req.predicate = NSPredicate(format: "needsReadStateSync = YES") if var needsSync = try? persistentContainer.viewContext.fetch(req) { let firstReadIndex = needsSync.partition(by: \.read) let unreadIDs = needsSync[..() if !unreadIDs.isEmpty { do { let ids = try await client.unread(ids: unreadIDs) updatedIDs.formUnion(ids) } catch { logger.error("Failed to sync unread state: \(String(describing: error), privacy: .public)") } } if !readIDs.isEmpty { do { let ids = try await client.read(ids: readIDs) updatedIDs.formUnion(ids) } catch { logger.error("Failed to sync read state: \(String(describing: error), privacy: .public)") } } count += updatedIDs.count for item in needsSync where updatedIDs.contains(item.id!) { item.needsReadStateSync = false } } 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 } }