frenzy-ios/Reader/FervorController.swift

195 lines
6.8 KiB
Swift
Raw Normal View History

2021-12-08 02:58:02 +00:00
//
// FervorController.swift
// Reader
//
// Created by Shadowfacts on 11/25/21.
//
import Foundation
2021-12-08 02:58:02 +00:00
import Fervor
import OSLog
2022-01-27 03:37:10 +00:00
import Combine
import Persistence
2021-12-08 02:58:02 +00:00
actor FervorController {
2021-12-08 02:58:02 +00:00
static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")!
let instanceURL: URL
2022-01-09 22:13:30 +00:00
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController")
let client: FervorClient
nonisolated let account: LocalData.Account?
2021-12-08 02:58:02 +00:00
private(set) var clientID: String?
private(set) var clientSecret: String?
2022-03-06 20:34:30 +00:00
private(set) var token: Token?
2021-12-08 02:58:02 +00:00
nonisolated let persistentContainer: PersistentContainer!
2021-12-25 19:04:45 +00:00
nonisolated let syncState = PassthroughSubject<SyncState, Never>()
private var lastSyncState = SyncState.done
private var cancellables = Set<AnyCancellable>()
2022-01-27 03:37:10 +00:00
2022-06-22 20:05:06 +00:00
init(instanceURL: URL, account: LocalData.Account?, session: URLSession = .shared) async {
2021-12-08 02:58:02 +00:00
self.instanceURL = instanceURL
2022-06-22 20:05:06 +00:00
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken, session: session)
2022-01-12 18:48:52 +00:00
self.account = account
self.clientID = account?.clientID
self.clientSecret = account?.clientSecret
2021-12-25 19:04:45 +00:00
if let account = account {
self.persistentContainer = PersistentContainer(account: account)
} else {
self.persistentContainer = nil
}
}
2022-06-22 20:05:06 +00:00
convenience init(account: LocalData.Account, session: URLSession = .shared) async {
await self.init(instanceURL: account.instanceURL, account: account, session: session)
2021-12-08 02:58:02 +00:00
}
2022-01-27 03:37:10 +00:00
private func setSyncState(_ state: SyncState) {
lastSyncState = state
syncState.send(state)
2022-01-27 03:37:10 +00:00
}
2021-12-08 02:58:02 +00:00
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 {
2022-03-06 20:34:30 +00:00
token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!)
2021-12-08 02:58:02 +00:00
}
func syncAll() async throws {
2022-06-25 22:40:56 +00:00
guard lastSyncState.isFinished else {
return
}
2022-06-25 22:40:56 +00:00
do {
setSyncState(.groupsAndFeeds)
logger.info("Syncing groups and feeds")
let groups = try await client.groups()
let feeds = try await client.feeds()
2022-06-25 22:40:56 +00:00
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, setProgress: { count, total in self.setSyncState(.updateItems(current: count, total: total)) })
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
await ExcerptGenerator.generateAll(self, setProgress: { count, total in self.setSyncState(.excerpts(current: count, total: total)) })
await WidgetHelper.updateWidgetData(fervorController: self)
setSyncState(.done)
} catch {
setSyncState(.error(error))
throw error
}
2021-12-25 19:04:45 +00:00
}
2022-03-06 20:34:30 +00:00
@MainActor
func syncReadToServer() async {
2022-01-11 16:54:57 +00:00
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[..<firstReadIndex].map(\.id.unsafelyUnwrapped)
let readIDs = needsSync[firstReadIndex...].map(\.id.unsafelyUnwrapped)
var updatedIDs = Set<FervorID>()
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
}
}
2022-01-11 16:54:57 +00:00
logger.info("Synced \(count, privacy: .public) read/unread to server")
do {
try persistentContainer.viewContext.save()
} catch {
2022-01-27 02:22:15 +00:00
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
}
2022-01-11 16:54:57 +00:00
}
@MainActor
func markItem(_ item: Persistence.Item, read: Bool) async {
item.read = read
do {
let f = read ? client.read(item:) : client.unread(item:)
_ = try await f(item.id!)
item.needsReadStateSync = false
} catch {
2022-01-27 02:22:15 +00:00
logger.error("Failed to mark item (un)read: \(String(describing: error), privacy: .public)")
item.needsReadStateSync = true
}
do {
try self.persistentContainer.saveViewContext()
} catch {
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
}
}
@MainActor
func fetchItem(id: String) async throws -> Persistence.Item? {
guard let serverItem = try await client.item(id: id) else {
return nil
}
let item = Persistence.Item(context: persistentContainer.viewContext)
item.updateFromServer(serverItem)
try persistentContainer.saveViewContext()
return item
}
2021-12-08 02:58:02 +00:00
}
2022-01-27 03:37:10 +00:00
extension FervorController {
2022-06-25 22:40:56 +00:00
enum SyncState {
2022-01-27 03:37:10 +00:00
case groupsAndFeeds
case items
case updateItems(current: Int, total: Int)
2022-06-25 22:40:56 +00:00
case excerpts(current: Int, total: Int)
case error(Error)
2022-01-27 03:37:10 +00:00
case done
2022-06-25 22:40:56 +00:00
var isFinished: Bool {
switch self {
case .error(_), .done:
return true
default:
return false
}
}
2022-01-27 03:37:10 +00:00
}
}