Replace MastodonAuthentication to not be backed by CoreData

This commit is contained in:
Marcus Kida 2023-05-25 16:26:17 +02:00 committed by Nathan Mattes
parent c94f0126b7
commit d570d3ef09
62 changed files with 369 additions and 474 deletions

View File

@ -309,7 +309,6 @@
DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */; };
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */; };
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */; };
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */; };
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; };
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; };
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; };
@ -997,7 +996,6 @@
DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+Configuration.swift"; sourceTree = "<group>"; };
DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Reblog.swift"; sourceTree = "<group>"; };
DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Favorite.swift"; sourceTree = "<group>"; };
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAuthentication+Fetch.swift"; sourceTree = "<group>"; };
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = "<group>"; };
DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = "<group>"; };
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = "<group>"; };
@ -2302,7 +2300,6 @@
DB64BA462851F23300ADF1B7 /* Model */ = {
isa = PBXGroup;
children = (
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */,
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */,
);
path = Model;
@ -3949,7 +3946,6 @@
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */,
2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */,
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */,
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */,
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */,
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */,

View File

@ -54,12 +54,8 @@ final public class SceneCoordinator {
return
} else {
// switch to notification's account
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
request.returnsObjectsAsFaults = false
request.fetchLimit = 1
do {
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
guard let authentication = AuthenticationServiceProvider.shared.authentications.first(where: { $0.userAccessToken == accessToken }) else {
return
}
let domain = authentication.domain
@ -221,8 +217,7 @@ extension SceneCoordinator {
let rootViewController: UIViewController
do {
let request = MastodonAuthentication.activeSortedFetchRequest // use active order
let _authentication = try appContext.managedObjectContext.fetch(request).first
let _authentication = AuthenticationServiceProvider.shared.authentications.first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
self.authContext = _authContext

View File

@ -77,7 +77,7 @@ extension DiscoverySection {
cell.profileCardView.viewModel.familiarFollowers = nil
}
// bind me
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
}
return cell
case .bottomLoader:

View File

@ -31,7 +31,7 @@ extension SearchHistorySection {
context.managedObjectContext.performAndWait {
guard let user = item.object(in: context.managedObjectContext) else { return }
cell.configure(
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
viewModel: SearchHistoryUserCollectionViewCell.ViewModel(
value: user,
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),

View File

@ -129,7 +129,7 @@ extension SearchResultSection {
configuration: Configuration
) {
cell.configure(
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate

View File

@ -83,7 +83,7 @@ extension UserSection {
configuration: Configuration
) {
cell.configure(
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate

View File

@ -12,17 +12,13 @@ import MastodonSDK
extension AppContext {
func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? {
let request = MastodonAuthentication.sortedFetchRequest
guard
let accounts = try? managedObjectContext.fetch(request),
accounts.count > 1
else { return nil }
let accounts = AuthenticationServiceProvider.shared.authentications
guard accounts.count > 1 else { return nil }
let nextSelectedAccountIndex: Int? = {
for (index, account) in accounts.enumerated() {
guard account == authContext.mastodonAuthenticationBox
.authenticationRecord
.object(in: managedObjectContext)
.authentication
else { continue }
let nextAccountIndex = index + 1

View File

@ -23,7 +23,7 @@ extension DataSourceFacade {
let managedObjectContext = provider.context.backgroundManagedObjectContext
try? await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
guard let user = record.object(in: managedObjectContext) else { return }
_ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext,
@ -41,7 +41,7 @@ extension DataSourceFacade {
switch tag {
case .entity(let entity):
try? await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let now = Date()
@ -67,7 +67,7 @@ extension DataSourceFacade {
case .record(let record):
try? await managedObjectContext.performChanges {
let authenticationBox = provider.authContext.mastodonAuthenticationBox
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
guard let tag = record.object(in: managedObjectContext) else { return }
let now = Date()
@ -98,7 +98,7 @@ extension DataSourceFacade {
let managedObjectContext = provider.context.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let request = SearchHistory.sortedFetchRequest
request.predicate = SearchHistory.predicate(
domain: authenticationBox.domain,

View File

@ -22,10 +22,8 @@ final class AccountListViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
// output
@Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
@Published var items: [Item] = []
let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
@ -34,30 +32,11 @@ final class AccountListViewModel: NSObject {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.mastodonAuthenticationFetchedResultsController = {
let fetchRequest = MastodonAuthentication.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init()
// end init
mastodonAuthenticationFetchedResultsController.delegate = self
do {
try mastodonAuthenticationFetchedResultsController.performFetch()
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? []
} catch {
assertionFailure(error.localizedDescription)
}
$authentications
AuthenticationServiceProvider.shared.$authentications
.receive(on: DispatchQueue.main)
.sink { [weak self] authentications in
guard let self = self else { return }
@ -86,7 +65,7 @@ extension AccountListViewModel {
}
enum Item: Hashable {
case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
case authentication(record: MastodonAuthentication)
case addAccount
}
@ -98,12 +77,12 @@ extension AccountListViewModel {
switch item {
case .authentication(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
if let authentication = record.object(in: managedObjectContext),
let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
if let activeAuthentication = AuthenticationServiceProvider.shared.authentications.first
{
AccountListViewModel.configure(
in: managedObjectContext,
cell: cell,
authentication: authentication,
authentication: record,
activeAuthentication: activeAuthentication
)
}
@ -120,11 +99,12 @@ extension AccountListViewModel {
}
static func configure(
in context: NSManagedObjectContext,
cell: AccountListTableViewCell,
authentication: MastodonAuthentication,
activeAuthentication: MastodonAuthentication
) {
let user = authentication.user
guard let user = authentication.user(in: context) else { return }
// avatar
cell.avatarButton.avatarImageView.configure(
@ -169,21 +149,3 @@ extension AccountListViewModel {
.joined(separator: ", ")
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension AccountListViewModel: NSFetchedResultsControllerDelegate {
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? []
}
}

View File

@ -69,8 +69,7 @@ extension AccountListViewController: PanModalPresentable {
return .contentHeight(CGFloat(height))
}
let request = MastodonAuthentication.sortedFetchRequest
let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0
let authenticationCount = AuthenticationServiceProvider.shared.authentications.count
let count = authenticationCount + 1
let height = calculateHeight(of: count)
@ -177,9 +176,8 @@ extension AccountListViewController: UITableViewDelegate {
switch item {
case .authentication(let record):
assert(Thread.isMainThread)
guard let authentication = record.object(in: context.managedObjectContext) else { return }
Task { @MainActor in
let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
let isActive = try await context.authenticationService.activeMastodonUser(domain: record.domain, userID: record.userID)
guard isActive else { return }
self.coordinator.setup()
} // end Task

View File

@ -199,41 +199,26 @@ extension AuthenticationViewModel {
domain: info.domain,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
.tryMap { response -> Mastodon.Response.Content<Mastodon.Entity.Account> in
let account = response.value
let mastodonUserRequest = MastodonUser.sortedFetchRequest
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
mastodonUserRequest.fetchLimit = 1
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher()
throw AuthenticationError.badCredentials
}
AuthenticationServiceProvider.shared
.authentications
.insert(MastodonAuthentication.createFrom(domain: info.domain,
userID: mastodonUser.id,
username: mastodonUser.username,
appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken,
clientID: info.clientID,
clientSecret: info.clientSecret), at: 0)
let property = MastodonAuthentication.Property(
domain: info.domain,
userID: mastodonUser.id,
username: mastodonUser.username,
appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken,
clientID: info.clientID,
clientSecret: info.clientSecret
)
return managedObjectContext.performChanges {
_ = APIService.CoreData.createOrMergeMastodonAuthentication(
into: managedObjectContext,
for: mastodonUser,
in: info.domain,
property: property,
networkDate: response.networkDate
)
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .failure(let error): throw error
case .success: return response
}
}
.eraseToAnyPublisher()
return response
}
.eraseToAnyPublisher()
}

View File

@ -34,7 +34,7 @@ final class FollowedTagsViewModel: NSObject {
self.fetchedResultsController = FollowedTagsFetchedResultController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
user: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)!.user
user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme:
)
super.init()

View File

@ -16,7 +16,7 @@ import MastodonSDK
final class MeProfileViewModel: ProfileViewModel {
init(context: AppContext, authContext: AuthContext) {
let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
super.init(
context: context,
authContext: authContext,

View File

@ -963,11 +963,6 @@ extension ProfileViewController: PagerTabStripNavigateable {
private extension ProfileViewController {
var currentInstance: Instance? {
guard let authenticationRecord = authContext.mastodonAuthenticationBox
.authenticationRecord
.object(in: context.managedObjectContext)
else { return nil }
return authenticationRecord.instance
authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext)
}
}

View File

@ -85,7 +85,7 @@ class ProfileViewModel: NSObject {
super.init()
// bind me
self.me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
$me
.assign(to: \.me, on: relationshipViewModel)
.store(in: &disposeBag)

View File

@ -60,7 +60,7 @@ class ReportResultViewModel: ObservableObject {
Task { @MainActor in
guard let user = user.object(in: context.managedObjectContext) else { return }
guard let me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { return }
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return }
self.relationshipViewModel.user = user
self.relationshipViewModel.me = me

View File

@ -292,7 +292,7 @@ extension MainTabBarController {
}
.store(in: &disposeBag)
if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user {
if let user = authContext?.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) {
self.avatarURLObserver = user.publisher(for: \.avatar)
.sink { [weak self, weak user] _ in
guard let self = self else { return }
@ -486,9 +486,9 @@ extension MainTabBarController {
authenticationBox: authContext.mastodonAuthenticationBox
)
if let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(
if let user = authContext.mastodonAuthenticationBox.authentication.user(
in: context.managedObjectContext
)?.user {
) {
user.update(
property: .init(
entity: profileResponse.value,

View File

@ -75,7 +75,7 @@ extension SidebarViewModel {
let imageURL: URL? = {
switch item {
case .me:
let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user
let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext)
return user?.avatarImageURL()
default:
return nil
@ -132,7 +132,7 @@ extension SidebarViewModel {
}
.store(in: &cell.disposeBag)
case .me:
guard let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
guard let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
let currentUserDisplayName = user.displayNameWithFallback
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
default:

View File

@ -214,11 +214,8 @@ extension SceneDelegate {
assertionFailure()
return false
}
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
request.fetchLimit = 1
guard let authentication = try? coordinator.appContext.managedObjectContext.fetch(request).first else {
guard let authentication = AuthenticationServiceProvider.shared.getAuthentication(matching: accessToken) else {
assertionFailure()
return false
}

View File

@ -50,9 +50,8 @@ extension SendPostIntentHandler: SendPostIntentHandling {
let mastodonAuthentications: [MastodonAuthentication]
let accounts = intent.accounts ?? []
if accounts.isEmpty {
let request = MastodonAuthentication.sortedFetchRequest
let authentications = try managedObjectContext.fetch(request)
let _authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
// fixme: refactor this and implemented method on AuthenticationServiceProvider
let _authentication = AuthenticationServiceProvider.shared.authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
guard let authentication = _authentication else {
let failureReason = APIService.APIError.implicit(.authenticationMissing).errorDescription ?? "Fail to Send Post"
@ -65,12 +64,12 @@ extension SendPostIntentHandler: SendPostIntentHandling {
let authenticationBoxes = mastodonAuthentications.map { authentication in
MastodonAuthenticationBox(
authenticationRecord: .init(objectID: authentication.objectID),
authentication: authentication,
domain: authentication.domain,
userID: authentication.userID,
appAuthorization: .init(accessToken: authentication.appAccessToken),
userAuthorization: .init(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.objectID.description)
inMemoryCache: .sharedCache(for: authentication.userAccessToken)
)
}

View File

@ -17,9 +17,11 @@ extension Account {
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
// get accounts
let accounts: [Account] = try await managedObjectContext.perform {
let results = try MastodonAuthentication.fetch(in: managedObjectContext)
let results = AuthenticationServiceProvider.shared.authentications
let accounts = results.compactMap { mastodonAuthentication -> Account? in
let user = mastodonAuthentication.user
guard let user = mastodonAuthentication.user(in: managedObjectContext) else {
return nil
}
let account = Account(
identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback,
@ -43,9 +45,7 @@ extension Array where Element == Account {
let identifiers = self
.compactMap { $0.identifier }
.compactMap { UUID(uuidString: $0) }
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(identifiers: identifiers)
let results = try managedObjectContext.fetch(request)
let results = AuthenticationServiceProvider.shared.authentications.filter({ identifiers.contains($0.identifier) })
return results
}

View File

@ -1,20 +0,0 @@
//
// MastodonAuthentication.swift
// MastodonIntent
//
// Created by MainasuK on 2022-6-9.
//
import Foundation
import CoreData
import CoreDataStack
extension MastodonAuthentication {
static func fetch(in managedObjectContext: NSManagedObjectContext) throws -> [MastodonAuthentication] {
let request = MastodonAuthentication.sortedFetchRequest
let results = try managedObjectContext.fetch(request)
return results
}
}

View File

@ -19,7 +19,7 @@ public final class Instance: NSManagedObject {
@NSManaged public private(set) var configurationV2Raw: Data?
// MARK: one-to-many relationships
@NSManaged public var authentications: Set<MastodonAuthentication>
@NSManaged public var authentications: Set<MastodonAuthenticationLegacy>
}
extension Instance {

View File

@ -8,7 +8,8 @@
import Foundation
import CoreData
final public class MastodonAuthentication: NSManagedObject {
@objc(MastodonAuthentication)
final public class MastodonAuthenticationLegacy: NSManagedObject {
public typealias ID = UUID
@ -35,16 +36,16 @@ final public class MastodonAuthentication: NSManagedObject {
}
extension MastodonAuthentication {
extension MastodonAuthenticationLegacy {
public override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthentication.identifier))
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthenticationLegacy.identifier))
let now = Date()
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.createdAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.updatedAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.activedAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.createdAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.updatedAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.activedAt))
}
@discardableResult
@ -52,8 +53,8 @@ extension MastodonAuthentication {
into context: NSManagedObjectContext,
property: Property,
user: MastodonUser
) -> MastodonAuthentication {
let authentication: MastodonAuthentication = context.insertObject()
) -> MastodonAuthenticationLegacy {
let authentication: MastodonAuthenticationLegacy = context.insertObject()
authentication.domain = property.domain
authentication.userID = property.userID
@ -112,7 +113,7 @@ extension MastodonAuthentication {
}
extension MastodonAuthentication {
extension MastodonAuthenticationLegacy {
public struct Property {
public let domain: String
@ -144,51 +145,51 @@ extension MastodonAuthentication {
}
}
extension MastodonAuthentication: Managed {
extension MastodonAuthenticationLegacy: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonAuthentication.createdAt, ascending: false)]
return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.createdAt, ascending: false)]
}
public static var activeSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonAuthentication.activedAt, ascending: false)]
return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.activedAt, ascending: false)]
}
}
extension MastodonAuthentication {
public static var activeSortedFetchRequest: NSFetchRequest<MastodonAuthentication> {
let request = NSFetchRequest<MastodonAuthentication>(entityName: entityName)
extension MastodonAuthenticationLegacy {
public static var activeSortedFetchRequest: NSFetchRequest<MastodonAuthenticationLegacy> {
let request = NSFetchRequest<MastodonAuthenticationLegacy>(entityName: entityName)
request.sortDescriptors = activeSortDescriptors
return request
}
}
extension MastodonAuthentication {
extension MastodonAuthenticationLegacy {
public static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.domain), domain)
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.domain), domain)
}
static func predicate(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userID), userID)
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.userID), userID)
}
public static func predicate(domain: String, userID: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonAuthentication.predicate(domain: domain),
MastodonAuthentication.predicate(userID: userID)
MastodonAuthenticationLegacy.predicate(domain: domain),
MastodonAuthenticationLegacy.predicate(userID: userID)
])
}
public static func predicate(userAccessToken: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userAccessToken), userAccessToken)
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.userAccessToken), userAccessToken)
}
public static func predicate(identifier: UUID) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.identifier), identifier as NSUUID)
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.identifier), identifier as NSUUID)
}
public static func predicate(identifiers: [UUID]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(MastodonAuthentication.identifier), identifiers as [NSUUID])
return NSPredicate(format: "%K IN %@", #keyPath(MastodonAuthenticationLegacy.identifier), identifiers as [NSUUID])
}
}

View File

@ -62,7 +62,7 @@ final public class MastodonUser: NSManagedObject {
// one-to-one relationship
@NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthenticationLegacy?
// one-to-many relationship
@NSManaged public private(set) var statuses: Set<Status>

View File

@ -27,38 +27,12 @@ public class AuthContext {
private init(mastodonAuthenticationBox: MastodonAuthenticationBox) {
self.mastodonAuthenticationBox = mastodonAuthenticationBox
}
}
extension AuthContext {
public convenience init?(authentication: MastodonAuthentication) {
self.init(mastodonAuthenticationBox: MastodonAuthenticationBox(authentication: authentication))
ManagedObjectObserver.observe(object: authentication)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
switch completion {
case .failure(let error):
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)")
case .finished:
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): observer finished")
}
} receiveValue: { [weak self] change in
guard let self = self else { return }
switch change.changeType {
case .update(let object):
guard let authentication = object as? MastodonAuthentication else {
assertionFailure()
return
}
self.mastodonAuthenticationBox = .init(authentication: authentication)
default:
break
}
}
.store(in: &disposeBag)
}
}

View File

@ -10,7 +10,7 @@ import CoreDataStack
import MastodonSDK
public struct MastodonAuthenticationBox: UserIdentifier {
public let authenticationRecord: ManagedObjectRecord<MastodonAuthentication>
public let authentication: MastodonAuthentication
public let domain: String
public let userID: MastodonUser.ID
public let appAuthorization: Mastodon.API.OAuth.Authorization
@ -18,14 +18,14 @@ public struct MastodonAuthenticationBox: UserIdentifier {
public let inMemoryCache: MastodonAccountInMemoryCache
public init(
authenticationRecord: ManagedObjectRecord<MastodonAuthentication>,
authentication: MastodonAuthentication,
domain: String,
userID: MastodonUser.ID,
appAuthorization: Mastodon.API.OAuth.Authorization,
userAuthorization: Mastodon.API.OAuth.Authorization,
inMemoryCache: MastodonAccountInMemoryCache
) {
self.authenticationRecord = authenticationRecord
self.authentication = authentication
self.domain = domain
self.userID = userID
self.appAuthorization = appAuthorization
@ -38,12 +38,12 @@ extension MastodonAuthenticationBox {
init(authentication: MastodonAuthentication) {
self = MastodonAuthenticationBox(
authenticationRecord: .init(objectID: authentication.objectID),
authentication: authentication,
domain: authentication.domain,
userID: authentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.objectID.description)
inMemoryCache: .sharedCache(for: authentication.userID) // todo: make sure this is really unique
)
}

View File

@ -0,0 +1,49 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import Combine
import CoreDataStack
import MastodonSDK
public class AuthenticationServiceProvider: ObservableObject {
public static let shared = AuthenticationServiceProvider()
private init() {}
@Published public var authentications: [MastodonAuthentication] = []
func update(instance: Instance, where domain: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain else { return authentication }
return authentication.updating(instance: instance)
}
}
func delete(authentication: MastodonAuthentication) {
authentications.removeAll(where: { $0 == authentication })
}
func activateAuthentication(in domain: String, for userID: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain, authentication.userID == userID else {
return authentication
}
return authentication.updating(activatedAt: Date())
}
}
func getAuthentication(in domain: String, for userID: String) -> MastodonAuthentication? {
authentications.first(where: { $0.domain == domain && $0.userID == userID })
}
}
// MARK: - Public
public extension AuthenticationServiceProvider {
func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? {
authentications.first(where: { $0.userAccessToken == userAccessToken })
}
func sortByActivation() { // fixme: why do we need this?
authentications = authentications.sorted(by: { $0.activedAt > $1.activedAt })
}
}

View File

@ -0,0 +1,103 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import CoreDataStack
import MastodonSDK
public struct MastodonAuthentication: Codable, Hashable {
public typealias ID = UUID
public private(set) var identifier: ID
public private(set) var domain: String
public private(set) var username: String
public private(set) var appAccessToken: String
public private(set) var userAccessToken: String
public private(set) var clientID: String
public private(set) var clientSecret: String
public private(set) var createdAt: Date
public private(set) var updatedAt: Date
public private(set) var activedAt: Date
public private(set) var userID: String
public private(set) var instanceObjectIdURI: URL?
public static func createFrom(
domain: String,
userID: String,
username: String,
appAccessToken: String,
userAccessToken: String,
clientID: String,
clientSecret: String
) -> Self {
let now = Date()
return MastodonAuthentication(
identifier: .init(), // fixme
domain: domain,
username: username,
appAccessToken: appAccessToken,
userAccessToken: userAccessToken,
clientID: clientID,
clientSecret: clientSecret,
createdAt: now,
updatedAt: now,
activedAt: now,
userID: userID,
instanceObjectIdURI: nil
)
}
func copy(
identifier: ID? = nil,
domain: String? = nil,
username: String? = nil,
appAccessToken: String? = nil,
userAccessToken: String? = nil,
clientID: String? = nil,
clientSecret: String? = nil,
createdAt: Date? = nil,
updatedAt: Date? = nil,
activedAt: Date? = nil,
userID: String? = nil,
instanceObjectIdURI: URL? = nil
) -> Self {
MastodonAuthentication(
identifier: identifier ?? self.identifier,
domain: domain ?? self.domain,
username: username ?? self.username,
appAccessToken: appAccessToken ?? self.appAccessToken,
userAccessToken: userAccessToken ?? self.userAccessToken,
clientID: clientID ?? self.clientID,
clientSecret: clientSecret ?? self.clientSecret,
createdAt: createdAt ?? self.createdAt,
updatedAt: updatedAt ?? self.updatedAt,
activedAt: activedAt ?? self.activedAt,
userID: userID ?? self.userID,
instanceObjectIdURI: instanceObjectIdURI ?? self.instanceObjectIdURI
)
}
public func instance(in context: NSManagedObjectContext) -> Instance? {
guard
let instanceObjectIdURI = instanceObjectIdURI,
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI)
else { return nil }
return try? context.existingObject(with: objectID) as? Instance
}
public func user(in context: NSManagedObjectContext) -> MastodonUser? {
let userPredicate = MastodonUser.predicate(domain: domain, id: userID)
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
}
func updating(instance: Instance) -> Self {
copy(instanceObjectIdURI: instance.objectID.uriRepresentation())
}
func updating(activatedAt: Date) -> Self {
copy(activedAt: activatedAt)
}
}

View File

@ -182,7 +182,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Tag.createOrMerge(

View File

@ -69,12 +69,15 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let me = authentication.user
let isBlocking = user.blockingBy.contains(me)
let isFollowing = user.followingBy.contains(me)
// toggle block state
@ -119,10 +122,13 @@ extension APIService {
}
try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
let me = authentication.user
switch result {
case .success(let response):

View File

@ -29,12 +29,15 @@ extension APIService {
// update bookmark state and retrieve bookmark context
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let me = authentication.user
let status = _status.reblog ?? _status
let isBookmarked = status.bookmarkedBy.contains(me)
status.update(bookmarked: !isBookmarked, by: me)
@ -64,10 +67,13 @@ extension APIService {
// update bookmark state
try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
let me = authentication.user
let status = _status.reblog ?? _status
switch result {
@ -114,7 +120,10 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
guard
let me = authenticationBox.authentication.user(in: managedObjectContext)
else {
assertionFailure()
return
}

View File

@ -31,12 +31,15 @@ extension APIService {
// update like state and retrieve like context
let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let me = authentication.user
let status = _status.reblog ?? _status
let isFavorited = status.favouritedBy.contains(me)
let favoritedCount = status.favouritesCount
@ -70,10 +73,13 @@ extension APIService {
// update like state
try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
let me = authentication.user
let status = _status.reblog ?? _status
switch result {
@ -124,7 +130,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}

View File

@ -38,7 +38,7 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return nil }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil }
guard let user = user.object(in: managedObjectContext) else { return nil }
let isFollowing = user.followingBy.contains(me)
@ -94,7 +94,7 @@ extension APIService {
// update friendship state
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user,
guard let me = authenticationBox.authentication.user(in: managedObjectContext),
let user = user.object(in: managedObjectContext)
else { return }
@ -129,10 +129,9 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
let me = authenticationBox.authentication.user(in: managedObjectContext)
else { throw APIError.implicit(.badRequest) }
let me = authentication.user
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
let oldShowReblogs = me.showingReblogsBy.contains(user)
@ -153,7 +152,7 @@ extension APIService {
}
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
switch result {
case .success(let response):

View File

@ -36,7 +36,7 @@ extension APIService {
)
request.fetchLimit = 1
guard let user = managedObjectContext.safeFetch(request).first else { return }
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
Persistence.MastodonUser.update(
mastodonUser: user,

View File

@ -36,7 +36,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge(

View File

@ -37,8 +37,8 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,

View File

@ -45,8 +45,8 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,

View File

@ -40,7 +40,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}

View File

@ -68,13 +68,15 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let me = authentication.user
let isMuting = user.mutingBy.contains(me)
// toggle mute state
@ -116,9 +118,8 @@ extension APIService {
try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
let me = authenticationBox.authentication.user(in: managedObjectContext)
else { return }
let me = authentication.user
switch result {
case .success(let response):

View File

@ -88,7 +88,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}
@ -176,7 +176,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
_ = Persistence.Notification.createOrMerge(
in: managedObjectContext,
context: Persistence.Notification.PersistContext(

View File

@ -36,7 +36,7 @@ extension APIService {
).singleOutput()
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Poll.createOrMerge(
in: managedObjectContext,
context: Persistence.Poll.PersistContext(
@ -79,7 +79,7 @@ extension APIService {
).singleOutput()
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Poll.createOrMerge(
in: managedObjectContext,
context: Persistence.Poll.PersistContext(

View File

@ -30,7 +30,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,

View File

@ -29,11 +29,13 @@ extension APIService {
// update repost state and retrieve repost context
let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let me = authentication.user(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
else { return nil }
let me = authentication.user
let status = _status.reblog ?? _status
let isReblogged = status.rebloggedBy.contains(me)
let rebloggedCount = status.reblogsCount
@ -70,10 +72,13 @@ extension APIService {
// update repost state
try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
let authentication = authenticationBox.authentication
guard
let me = authentication.user(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
else { return }
let me = authentication.user
let status = _status.reblog ?? _status
switch result {

View File

@ -42,7 +42,7 @@ extension APIService {
).singleOutput()
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
// assertionFailure()
return
}

View File

@ -28,7 +28,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
// user
for entity in response.value.accounts {

View File

@ -76,7 +76,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
let status = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(

View File

@ -34,7 +34,7 @@ extension APIService {
#if !APP_EXTENSION
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(

View File

@ -30,7 +30,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(

View File

@ -74,7 +74,7 @@ fileprivate extension APIService {
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Tag.createOrMerge(
in: managedObjectContext,

View File

@ -30,7 +30,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
let value = response.value.ancestors + response.value.descendants
for entity in value {

View File

@ -46,7 +46,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,

View File

@ -1,76 +0,0 @@
//
// APIService+CoreData+MastodonAuthentication.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/3.
//
import os.log
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService.CoreData {
public static func createOrMergeMastodonAuthentication(
into managedObjectContext: NSManagedObjectContext,
for authenticateMastodonUser: MastodonUser,
in domain: String,
property: MastodonAuthentication.Property,
networkDate: Date
) -> (mastodonAuthentication: MastodonAuthentication, isCreated: Bool) {
// fetch old mastodon authentication
let oldMastodonAuthentication: MastodonAuthentication? = {
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: property.userID)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
return try managedObjectContext.fetch(request).first
} catch {
assertionFailure(error.localizedDescription)
return nil
}
}()
if let oldMastodonAuthentication = oldMastodonAuthentication {
// merge old mastodon authentication
APIService.CoreData.mergeMastodonAuthentication(
for: authenticateMastodonUser,
old: oldMastodonAuthentication,
in: domain,
property: property,
networkDate: networkDate
)
return (oldMastodonAuthentication, false)
} else {
let mastodonAuthentication = MastodonAuthentication.insert(
into: managedObjectContext,
property: property,
user: authenticateMastodonUser
)
return (mastodonAuthentication, true)
}
}
static func mergeMastodonAuthentication(
for authenticateMastodonUser: MastodonUser,
old authentication: MastodonAuthentication,
in domain: String,
property: MastodonAuthentication.Property,
networkDate: Date
) {
guard networkDate > authentication.updatedAt else { return }
authentication.update(username: property.username)
authentication.update(appAccessToken: property.appAccessToken)
authentication.update(userAccessToken: property.userAccessToken)
authentication.update(clientID: property.clientID)
authentication.update(clientSecret: property.clientSecret)
authentication.didUpdate(at: networkDate)
}
}

View File

@ -22,10 +22,9 @@ public final class AuthenticationService: NSObject {
weak var apiService: APIService?
let managedObjectContext: NSManagedObjectContext // read-only
let backgroundManagedObjectContext: NSManagedObjectContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
let authenticationServiceProvider = AuthenticationServiceProvider.shared
// output
@Published public var mastodonAuthentications: [ManagedObjectRecord<MastodonAuthentication>] = []
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
private func fetchFollowedBlockedUserIds(
@ -93,21 +92,8 @@ public final class AuthenticationService: NSObject {
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
super.init()
$mastodonAuthenticationBoxes
.sink { [weak self] boxes in
@ -123,10 +109,9 @@ public final class AuthenticationService: NSObject {
// TODO: verify credentials for active authentication
$mastodonAuthentications
authenticationServiceProvider.$authentications
.map { authentications -> [MastodonAuthenticationBox] in
return authentications
.compactMap { $0.object(in: managedObjectContext) }
.sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { authentication -> MastodonAuthenticationBox? in
return MastodonAuthenticationBox(authentication: authentication)
@ -134,14 +119,7 @@ public final class AuthenticationService: NSObject {
}
.assign(to: &$mastodonAuthenticationBoxes)
do {
try mastodonAuthenticationFetchedResultsController.performFetch()
mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?
.sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { $0.asRecord } ?? []
} catch {
assertionFailure(error.localizedDescription)
}
AuthenticationServiceProvider.shared.sortByActivation()
}
}
@ -151,18 +129,9 @@ extension AuthenticationService {
public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool {
var isActive = false
let managedObjectContext = backgroundManagedObjectContext
try await 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
}
mastodonAuthentication.update(activedAt: Date())
isActive = true
}
AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID)
isActive = true
return isActive
}
@ -183,12 +152,7 @@ extension AuthenticationService {
managedObjectContext.delete(feed)
}
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) else {
assertionFailure()
throw APIService.APIError.implicit(.authenticationMissing)
}
managedObjectContext.delete(authentication)
AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication)
}
// cancel push notification subscription
@ -203,23 +167,3 @@ extension AuthenticationService {
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension AuthenticationService: NSFetchedResultsControllerDelegate {
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?
.sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { $0.asRecord } ?? []
}
}

View File

@ -89,18 +89,8 @@ extension InstanceService {
log: Logger(subsystem: "Update", category: "InstanceService")
)
// update relationship
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
// update instance
AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
}
.setFailureType(to: Error.self)
.tryMap { result in
@ -128,18 +118,8 @@ extension InstanceService {
)
)
// update relationship
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
// update instance
AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
}
.setFailureType(to: Error.self)
.tryMap { result in

View File

@ -42,7 +42,7 @@ public final class NotificationService {
self.apiService = apiService
self.authenticationService = authenticationService
authenticationService.$mastodonAuthentications
AuthenticationServiceProvider.shared.$authentications
.sink(receiveValue: { [weak self] mastodonAuthentications in
guard let self = self else { return }
@ -113,13 +113,13 @@ extension NotificationService {
let managedObjectContext = authenticationService.managedObjectContext
return try await managedObjectContext.perform {
var items: [UIApplicationShortcutItem] = []
for object in authenticationService.mastodonAuthentications {
guard let authentication = managedObjectContext.object(with: object.objectID) as? MastodonAuthentication else { continue }
for authentication in AuthenticationServiceProvider.shared.authentications {
guard let user = authentication.user(in: managedObjectContext) else { continue }
let accessToken = authentication.userAccessToken
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
guard count > 0 else { continue }
let title = "@\(authentication.user.acctWithDomain)"
let title = "@\(user.acctWithDomain)"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
let item = UIApplicationShortcutItem(
@ -214,9 +214,8 @@ extension NotificationService {
let needsCancelSubscription: Bool = try await managedObjectContext.perform {
// check authentication exists
let authenticationRequest = MastodonAuthentication.sortedFetchRequest
authenticationRequest.predicate = MastodonAuthentication.predicate(userAccessToken: userAccessToken)
return managedObjectContext.safeFetch(authenticationRequest).first == nil
let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == userAccessToken }
return results.first == nil
}
guard needsCancelSubscription else {
@ -255,22 +254,17 @@ extension NotificationService {
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
guard let authenticationService = self.authenticationService else { return nil }
let managedObjectContext = authenticationService.managedObjectContext
return try await managedObjectContext.perform {
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken)
request.fetchLimit = 1
guard let authentication = managedObjectContext.safeFetch(request).first else { return nil }
return MastodonAuthenticationBox(
authenticationRecord: .init(objectID: authentication.objectID),
domain: authentication.domain,
userID: authentication.userID,
appAuthorization: .init(accessToken: authentication.appAccessToken),
userAuthorization: .init(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.objectID.description)
)
}
let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken }
guard let authentication = results.first else { return nil }
return MastodonAuthenticationBox(
authentication: authentication,
domain: authentication.domain,
userID: authentication.userID,
appAuthorization: .init(accessToken: authentication.appAccessToken),
userAuthorization: .init(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.userAccessToken)
)
}
}

View File

@ -159,7 +159,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
self.visibility = {
// default private when user locked
var visibility: Mastodon.Entity.Status.Visibility = {
guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else {
guard let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else {
return .public
}
return author.locked ? .private : .public
@ -227,7 +227,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
assertionFailure()
return
}
let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
var mentionAccts: [String] = []
if author?.id != status.author.id {
@ -262,9 +262,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
let _configuration: Mastodon.Entity.Instance.Configuration? = {
var configuration: Mastodon.Entity.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
else { return }
configuration = authentication.instance?.configuration
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configuration
}
return configuration
}()
@ -325,7 +324,7 @@ extension ComposeContentViewModel {
$authContext
.sink { [weak self] authContext in
guard let self = self else { return }
guard let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
self.avatarURL = user.avatarImageURL()
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
self.username = user.acctWithDomain
@ -564,7 +563,7 @@ extension ComposeContentViewModel {
let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>?
managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
}
guard let author = _author else {
throw AppError.badAuthentication
@ -620,7 +619,7 @@ extension ComposeContentViewModel {
let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>?
managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
}
guard let author = _author else {
throw AppError.badAuthentication

View File

@ -230,9 +230,8 @@ extension NotificationView.ViewModel {
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
else { return }
configuration = authentication.instance?.configurationV2
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
}
return configuration
}()

View File

@ -691,9 +691,8 @@ extension StatusView.ViewModel {
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
else { return }
configuration = authentication.instance?.configurationV2
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
}
return configuration
}()

View File

@ -179,8 +179,8 @@ extension ShareViewController {
extension ShareViewController {
private func setupAuthContext() throws -> AuthContext? {
let request = MastodonAuthentication.activeSortedFetchRequest // use active order
let _authentication = try context.managedObjectContext.fetch(request).first
AuthenticationServiceProvider.shared.sortByActivation()
let _authentication = AuthenticationServiceProvider.shared.authentications.first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
return _authContext
}

View File

@ -86,9 +86,9 @@ private extension FollowersCountWidgetProvider {
}
guard
let desiredAccount = configuration.account ?? authBox.authenticationRecord.object(
let desiredAccount = configuration.account ?? authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext
)?.user.acctWithDomain
)?.acctWithDomain
else {
return completion(.unconfigured)
}

View File

@ -86,9 +86,9 @@ private extension MultiFollowersCountWidgetProvider {
if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) {
desiredAccounts = configuredAccounts
} else if let currentlyLoggedInAccount = authBox.authenticationRecord.object(
} else if let currentlyLoggedInAccount = authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext
)?.user.acctWithDomain {
)?.acctWithDomain {
desiredAccounts = [currentlyLoggedInAccount]
} else {
return completion(.unconfigured)