// // AuthenticationService.swift // Mastodon // // Created by MainasuK Cirno on 2021/2/3. // import os.log import Foundation import Combine import CoreData import CoreDataStack import MastodonSDK final class AuthenticationService: NSObject { var disposeBag = Set() // input weak var apiService: APIService? let managedObjectContext: NSManagedObjectContext // read-only let backgroundManagedObjectContext: NSManagedObjectContext let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController // output let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([]) let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([]) let activeMastodonAuthentication = CurrentValueSubject(nil) let activeMastodonAuthenticationBox = CurrentValueSubject(nil) let activeFilters = CurrentValueSubject<[Mastodon.Entity.Filter], Never>([]) init( managedObjectContext: NSManagedObjectContext, backgroundManagedObjectContext: NSManagedObjectContext, apiService: APIService ) { self.managedObjectContext = managedObjectContext self.backgroundManagedObjectContext = backgroundManagedObjectContext self.apiService = apiService self.mastodonAuthenticationFetchedResultsController = { let fetchRequest = MastodonAuthentication.sortedFetchRequest fetchRequest.returnsObjectsAsFaults = false fetchRequest.fetchBatchSize = 20 let controller = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil ) return controller }() super.init() mastodonAuthenticationFetchedResultsController.delegate = self // TODO: verify credentials for active authentication // bind data mastodonAuthentications .map { $0.sorted(by: { $0.activedAt > $1.activedAt }).first } .assign(to: \.value, on: activeMastodonAuthentication) .store(in: &disposeBag) mastodonAuthentications .map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in return authentications .sorted(by: { $0.activedAt > $1.activedAt }) .compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in return AuthenticationService.MastodonAuthenticationBox( domain: authentication.domain, userID: authentication.userID, appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) ) } } .assign(to: \.value, on: mastodonAuthenticationBoxes) .store(in: &disposeBag) mastodonAuthenticationBoxes .map { $0.first } .assign(to: \.value, on: activeMastodonAuthenticationBox) .store(in: &disposeBag) do { try mastodonAuthenticationFetchedResultsController.performFetch() mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] } catch { assertionFailure(error.localizedDescription) } // fetch account filters every 60s and filter out expired items let filterUpdateTimerPublisher = Timer.publish(every: 60.0, on: .main, in: .common) .autoconnect() .share() .eraseToAnyPublisher() let filterUpdatePublisher = PassthroughSubject() filterUpdateTimerPublisher .map { _ in } .subscribe(filterUpdatePublisher) .store(in: &disposeBag) Publishers.CombineLatest( activeMastodonAuthenticationBox, filterUpdatePublisher ) .flatMap { box, _ -> AnyPublisher, Error>, Never> in guard let box = box else { return Just(Result { throw APIService.APIError.implicit(.authenticationMissing) }).eraseToAnyPublisher() } return apiService.filters(mastodonAuthenticationBox: box) .map { response in let now = Date() let newResponse = response.map { filters in return filters.filter { $0.expiresAt > now } } return Result, Error>.success(newResponse) } .catch { error in Just(Result, Error>.failure(error)) } .eraseToAnyPublisher() } .sink { result in switch result { case .success(let response): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters success. %ld items", ((#file as NSString).lastPathComponent), #line, #function, response.value.count) self.activeFilters.value = response.value case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) break } } .store(in: &disposeBag) filterUpdatePublisher.send() } } extension AuthenticationService { struct MastodonAuthenticationBox { let domain: String let userID: MastodonUser.ID let appAuthorization: Mastodon.API.OAuth.Authorization let userAuthorization: Mastodon.API.OAuth.Authorization } } extension AuthenticationService { func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { var isActive = false return backgroundManagedObjectContext.performChanges { let request = MastodonAuthentication.sortedFetchRequest request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) request.fetchLimit = 1 guard let mastodonAuthentication = try? self.backgroundManagedObjectContext.fetch(request).first else { return } mastodonAuthentication.update(activedAt: Date()) isActive = true } .map { result in return result.map { isActive } } .eraseToAnyPublisher() } func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { var isSignOut = false var _mastodonAuthenticationBox: MastodonAuthenticationBox? let managedObjectContext = backgroundManagedObjectContext return managedObjectContext.performChanges { let request = MastodonAuthentication.sortedFetchRequest request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) request.fetchLimit = 1 guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else { return } _mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox( domain: mastodonAuthentication.domain, userID: mastodonAuthentication.userID, appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken), userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) ) managedObjectContext.delete(mastodonAuthentication) isSignOut = true } .flatMap { result -> AnyPublisher, Never> in guard let apiService = self.apiService, let mastodonAuthenticationBox = _mastodonAuthenticationBox else { return Just(result).eraseToAnyPublisher() } return apiService.cancelSubscription( mastodonAuthenticationBox: mastodonAuthenticationBox ) .map { _ in result } .catch { _ in Just(result).eraseToAnyPublisher() } .eraseToAnyPublisher() } .map { result in return result.map { isSignOut } } .eraseToAnyPublisher() } } // MARK: - NSFetchedResultsControllerDelegate extension AuthenticationService: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { if controller === mastodonAuthenticationFetchedResultsController { mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] } } }