Merge pull request #1131 from mastodon/move-credentials-to-keychain

Use Keychain for credentials
This commit is contained in:
Nathan Mattes 2023-10-12 15:09:12 +02:00 committed by GitHub
commit 8381a44b71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 564 additions and 492 deletions

View File

@ -331,7 +331,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 */; };
@ -1037,7 +1036,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>"; };
@ -2404,7 +2402,6 @@
DB64BA462851F23300ADF1B7 /* Model */ = {
isa = PBXGroup;
children = (
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */,
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */,
);
path = Model;
@ -4057,7 +4054,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

@ -57,12 +57,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
@ -226,8 +222,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.authenticationSortedByActivation().first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
self.authContext = _authContext
@ -538,7 +533,7 @@ private extension SceneCoordinator {
viewController = activityViewController
case .settings(let setting):
guard let presentedOn = sender,
let accountName = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: appContext.managedObjectContext)?.username,
let accountName = authContext?.mastodonAuthenticationBox.authentication.username,
let authContext
else { return nil }

View File

@ -74,7 +74,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

@ -80,7 +80,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

@ -24,7 +24,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,
@ -42,7 +42,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()
@ -68,7 +68,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()
@ -99,7 +99,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

@ -21,10 +21,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>()
@ -33,30 +31,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 }
@ -85,7 +64,7 @@ extension AccountListViewModel {
}
enum Item: Hashable {
case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
case authentication(record: MastodonAuthentication)
case addAccount
}
@ -97,12 +76,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.authenticationSortedByActivation().first
{
AccountListViewModel.configure(
in: managedObjectContext,
cell: cell,
authentication: authentication,
authentication: record,
activeAuthentication: activeAuthentication
)
}
@ -119,11 +98,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(
@ -168,16 +148,3 @@ extension AccountListViewModel {
.joined(separator: ", ")
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension AccountListViewModel: NSFetchedResultsControllerDelegate {
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? []
}
}

View File

@ -66,8 +66,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)
@ -165,9 +164,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

@ -211,7 +211,8 @@ extension HomeTimelineViewController {
let userDoesntFollowPeople: Bool
if let managedObjectContext = self?.context.managedObjectContext,
let me = self?.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user {
let authContext = self?.authContext,
let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){
userDoesntFollowPeople = me.followersCount == 0
} else {
userDoesntFollowPeople = true

View File

@ -292,10 +292,9 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
snapshot.appendSections([MastodonLoginViewSection.servers])
snapshot.appendItems(viewModel.filteredServers)
dataSource?.apply(snapshot, animatingDifferences: false)
DispatchQueue.main.async {
self.dataSource?.apply(snapshot, animatingDifferences: false)
let numberOfResults = viewModel.filteredServers.count
self.contentView.updateCorners(numberOfResults: numberOfResults)
}

View File

@ -193,41 +193,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

@ -33,7 +33,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

@ -98,6 +98,7 @@ extension ProfileHeaderView.ViewModel {
// follows you
$relationshipActionOptionSet
.map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
.receive(on: DispatchQueue.main)
.sink { isFollowingBy in
view.followsYouBlurEffectView.isHidden = !isFollowingBy
}
@ -182,16 +183,19 @@ extension ProfileHeaderView.ViewModel {
}
.store(in: &disposeBag)
$relationshipActionOptionSet
.receive(on: DispatchQueue.main)
.sink { optionSet in
let isBlocking = optionSet.contains(.blocking)
let isBlockedBy = optionSet.contains(.blockingBy)
let isSuspended = optionSet.contains(.suspended)
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
view.bioMetaText.textView.isHidden = isNeedsHidden
}
.store(in: &disposeBag)
// dashboard
$isMyself
.receive(on: DispatchQueue.main)
.sink { isMyself in
if isMyself {
view.statusDashboardView.postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.myPosts
@ -246,6 +250,7 @@ extension ProfileHeaderView.ViewModel {
$isEditing,
$isUpdating
)
.receive(on: DispatchQueue.main)
.sink { relationshipActionOptionSet, isEditing, isUpdating in
if relationshipActionOptionSet.contains(.edit) {
// check .edit state and set .editing when isEditing

View File

@ -15,7 +15,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,
@ -29,5 +29,27 @@ final class MeProfileViewModel: ProfileViewModel {
}
.store(in: &disposeBag)
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
_ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
try await context.managedObjectContext.performChanges {
guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else {
assertionFailure()
return
}
self.me = me
}
} catch {
// do nothing?
}
}
}
}

View File

@ -273,6 +273,8 @@ extension ProfileViewController {
bindTitleView()
bindMoreBarButtonItem()
bindPager()
viewModel.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
@ -935,11 +937,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

@ -82,7 +82,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)
@ -171,9 +171,10 @@ class ProfileViewModel: NSObject {
.assign(to: &$isPagingEnabled)
}
}
extension ProfileViewModel {
func viewDidLoad() {
}
// fetch profile info before edit
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {

View File

@ -59,7 +59,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

@ -258,28 +258,34 @@ extension MainTabBarController {
}
.store(in: &disposeBag)
if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user {
self.avatarURLObserver = user.publisher(for: \.avatar)
.sink { [weak self, weak user] _ in
guard let self = self else { return }
guard let user = user else { return }
guard user.managedObjectContext != nil else { return }
self.avatarURL = user.avatarImageURL()
}
NotificationCenter.default.publisher(for: .userFetched)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) {
self.avatarURLObserver = user.publisher(for: \.avatar)
.sink { [weak self, weak user] _ in
guard let self = self else { return }
guard let user = user else { return }
guard user.managedObjectContext != nil else { return }
self.avatarURL = user.avatarImageURL()
}
// a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
guard let profileTabItem = _profileTabItem else { return }
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
// a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
guard let profileTabItem = _profileTabItem else { return }
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
context.authenticationService.updateActiveUserAccountPublisher
.sink { [weak self] in
self?.updateUserAccount()
self.context.authenticationService.updateActiveUserAccountPublisher
.sink { [weak self] in
self?.updateUserAccount()
}
.store(in: &self.disposeBag)
} else {
self.avatarURLObserver = nil
}
.store(in: &disposeBag)
} else {
self.avatarURLObserver = nil
}
}
.store(in: &disposeBag)
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
@ -451,9 +457,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

@ -79,7 +79,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext(
domain: authContext.mastodonAuthenticationBox.domain,
entity: status,
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user,
me: authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext),
statusCache: nil,
userCache: nil,
networkDate: Date()))

View File

@ -126,7 +126,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

@ -17,6 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let appContext = AppContext()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AuthenticationServiceProvider.shared.restore()
AppSecret.default.register()

View File

@ -185,11 +185,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

@ -65,7 +65,7 @@
<attribute name="version" optional="YES" attributeType="String"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthenticationLegacy" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/>

View File

@ -65,7 +65,7 @@
<attribute name="version" optional="YES" attributeType="String"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthenticationLegacy" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/>

View File

@ -20,9 +20,14 @@ public final class CoreDataStack {
self.storeDescriptions = storeDescriptions
}
public convenience init(databaseName: String = "shared") {
public convenience init(databaseName: String = "shared", isInMemory: Bool = false) {
let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
let storeDescription: NSPersistentStoreDescription
if isInMemory {
storeDescription = NSPersistentStoreDescription(url: URL(string: "file:///dev/null")!) /// in-memory store with all features in favor of NSInMemoryStoreType
} else {
storeDescription = NSPersistentStoreDescription(url: storeURL)
}
self.init(persistentStoreDescriptions: [storeDescription])
}
@ -115,16 +120,18 @@ extension CoreDataStack {
}
}
extension CoreDataStack {
public func rebuild() {
public extension CoreDataStack {
func tearDown() {
let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!)
try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil)
}
func rebuild() {
tearDown()
CoreDataStack.load(persistentContainer: persistentContainer) { [weak self] in
guard let self = self else { return }
self.didFinishLoad.value = true
}
}
}

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

@ -46,9 +46,18 @@ public class AppContext: ObservableObject {
.eraseToAnyPublisher()
public init() {
let authProvider = AuthenticationServiceProvider.shared
let _coreDataStack = CoreDataStack()
if authProvider.authenticationMigrationRequired {
authProvider.migrateLegacyAuthentications(
in: _coreDataStack.persistentContainer.viewContext
)
}
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()
coreDataStack = _coreDataStack
managedObjectContext = _managedObjectContext
backgroundManagedObjectContext = _backgroundManagedObjectContext

View File

@ -24,31 +24,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 { _ in
} 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,116 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import Combine
import CoreDataStack
import MastodonSDK
import KeychainAccess
import MastodonCommon
import os.log
public class AuthenticationServiceProvider: ObservableObject {
private let logger = Logger(subsystem: "AuthenticationServiceProvider", category: "Authentication")
public static let shared = AuthenticationServiceProvider()
private static let keychain = Keychain(service: "org.joinmastodon.app.authentications", accessGroup: AppName.groupID)
private let userDefaults: UserDefaults = .shared
private init() {}
@Published public var authentications: [MastodonAuthentication] = [] {
didSet {
persist() // todo: Is this too heavy and too often here???
}
}
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 authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this?
return authentications.sorted(by: { $0.activedAt > $1.activedAt })
}
func restore() {
authentications = Self.keychain.allKeys().compactMap {
guard
let encoded = Self.keychain[$0],
let data = Data(base64Encoded: encoded)
else { return nil }
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
}
}
func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
do {
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in
return MastodonAuthentication(
identifier: auth.identifier,
domain: auth.domain,
username: auth.username,
appAccessToken: auth.appAccessToken,
userAccessToken: auth.userAccessToken,
clientID: auth.clientID,
clientSecret: auth.clientSecret,
createdAt: auth.createdAt,
updatedAt: auth.updatedAt,
activedAt: auth.activedAt,
userID: auth.userID
)
}
if migratedAuthentications.count != legacyAuthentications.count {
logger.log(level: .default, "Not all account authentications could be migrated.")
} else {
logger.log(level: .default, "All account authentications were successful.")
}
self.authentications = migratedAuthentications
userDefaults.didMigrateAuthentications = true
} catch {
userDefaults.didMigrateAuthentications = false
logger.log(level: .error, "Could not migrate legacy authentications")
}
}
var authenticationMigrationRequired: Bool {
userDefaults.didMigrateAuthentications == false
}
}
// MARK: - Private
private extension AuthenticationServiceProvider {
func persist() {
for authentication in authentications {
Self.keychain[authentication.persistenceIdentifier] = try? JSONEncoder().encode(authentication).base64EncodedString()
}
}
}

View File

@ -0,0 +1,107 @@
// 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?
internal var persistenceIdentifier: String {
"\(username)@\(domain)"
}
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(),
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

@ -167,7 +167,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

@ -67,12 +67,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
@ -116,10 +119,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

@ -27,12 +27,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)
@ -60,10 +63,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 {
@ -108,7 +114,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

@ -28,12 +28,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
@ -65,10 +68,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 {
@ -117,7 +123,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

@ -36,7 +36,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)
@ -88,7 +88,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 }
@ -120,10 +120,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)
@ -144,7 +143,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

@ -35,7 +35,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

@ -35,7 +35,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

@ -36,8 +36,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

@ -44,8 +44,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

@ -11,6 +11,10 @@ import CoreData
import CoreDataStack
import MastodonSDK
public extension Foundation.Notification.Name {
static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched")
}
extension APIService {
public func homeTimeline(
@ -38,9 +42,21 @@ extension APIService {
).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
// FIXME: This is a dirty hack to make the performance-stuff work.
// Problem is, that we don't persist the user on disk anymore. So we have to fetch
// it when we need it to display on the home timeline.
for authentication in AuthenticationServiceProvider.shared.authentications {
_ = try await accountInfo(domain: authentication.domain,
userID: authentication.userID,
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value
}
NotificationCenter.default.post(name: .userFetched, object: nil)
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
assertionFailure()
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}

View File

@ -66,13 +66,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
@ -112,9 +114,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

@ -35,7 +35,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(
@ -78,7 +78,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

@ -29,7 +29,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

@ -27,11 +27,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
@ -66,10 +68,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

@ -41,7 +41,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

@ -27,7 +27,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

@ -33,7 +33,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

@ -29,7 +29,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

@ -73,7 +73,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

@ -29,7 +29,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

@ -45,7 +45,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,75 +0,0 @@
//
// APIService+CoreData+MastodonAuthentication.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/3.
//
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

@ -21,10 +21,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(
@ -92,21 +91,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
@ -122,10 +108,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)
@ -133,14 +118,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.authentications = AuthenticationServiceProvider.shared.authenticationSortedByActivation()
}
}
@ -150,18 +128,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
}
@ -182,12 +151,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
@ -202,19 +166,3 @@ extension AuthenticationService {
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension AuthenticationService: NSFetchedResultsControllerDelegate {
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

@ -78,18 +78,8 @@ extension InstanceService {
networkDate: response.networkDate
)
// 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
@ -116,18 +106,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

@ -41,7 +41,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 }
@ -100,13 +100,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(
@ -201,9 +201,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 {
@ -240,22 +239,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

@ -0,0 +1,20 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
public extension UserDefaults {
enum Keys {
static let didMigrateAuthenticationsKey = "didMigrateAuthentications"
}
@objc dynamic var didMigrateAuthentications: Bool {
get {
return bool(forKey: Keys.didMigrateAuthenticationsKey)
}
set {
set(newValue, forKey: Keys.didMigrateAuthenticationsKey)
}
}
}

View File

@ -156,7 +156,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
@ -224,7 +224,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 {
@ -259,9 +259,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
}()
@ -319,7 +318,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
@ -565,7 +564,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
@ -621,7 +620,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

@ -237,9 +237,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

@ -686,9 +686,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

@ -126,6 +126,7 @@ public final class RelationshipViewModel {
$me,
relationshipUpdatePublisher
)
.receive(on: DispatchQueue.main)
.sink { [weak self] user, me, _ in
guard let self = self else { return }
self.update(user: user, me: me)

View File

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

View File

@ -83,9 +83,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)