163 lines
5.6 KiB
Swift
163 lines
5.6 KiB
Swift
//
|
|
// 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<SyncState, Never>()
|
|
|
|
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
|
|
}
|
|
}
|