Merge pull request #1131 from mastodon/move-credentials-to-keychain
Use Keychain for credentials
This commit is contained in:
commit
8381a44b71
@ -331,7 +331,6 @@
|
|||||||
DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; };
|
||||||
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = "<group>"; };
|
||||||
@ -2404,7 +2402,6 @@
|
|||||||
DB64BA462851F23300ADF1B7 /* Model */ = {
|
DB64BA462851F23300ADF1B7 /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */,
|
|
||||||
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */,
|
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
@ -4057,7 +4054,6 @@
|
|||||||
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */,
|
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */,
|
||||||
2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
|
2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
|
||||||
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */,
|
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */,
|
||||||
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */,
|
|
||||||
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */,
|
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */,
|
||||||
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */,
|
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */,
|
||||||
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */,
|
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */,
|
||||||
|
@ -57,12 +57,8 @@ final public class SceneCoordinator {
|
|||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// switch to notification's account
|
// switch to notification's account
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
|
||||||
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
|
|
||||||
request.returnsObjectsAsFaults = false
|
|
||||||
request.fetchLimit = 1
|
|
||||||
do {
|
do {
|
||||||
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
|
guard let authentication = AuthenticationServiceProvider.shared.authentications.first(where: { $0.userAccessToken == accessToken }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let domain = authentication.domain
|
let domain = authentication.domain
|
||||||
@ -226,8 +222,7 @@ extension SceneCoordinator {
|
|||||||
let rootViewController: UIViewController
|
let rootViewController: UIViewController
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let request = MastodonAuthentication.activeSortedFetchRequest // use active order
|
let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
|
||||||
let _authentication = try appContext.managedObjectContext.fetch(request).first
|
|
||||||
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
||||||
self.authContext = _authContext
|
self.authContext = _authContext
|
||||||
|
|
||||||
@ -538,7 +533,7 @@ private extension SceneCoordinator {
|
|||||||
viewController = activityViewController
|
viewController = activityViewController
|
||||||
case .settings(let setting):
|
case .settings(let setting):
|
||||||
guard let presentedOn = sender,
|
guard let presentedOn = sender,
|
||||||
let accountName = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: appContext.managedObjectContext)?.username,
|
let accountName = authContext?.mastodonAuthenticationBox.authentication.username,
|
||||||
let authContext
|
let authContext
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ extension DiscoverySection {
|
|||||||
cell.profileCardView.viewModel.familiarFollowers = nil
|
cell.profileCardView.viewModel.familiarFollowers = nil
|
||||||
}
|
}
|
||||||
// bind me
|
// 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
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
|
@ -80,7 +80,7 @@ extension UserSection {
|
|||||||
configuration: Configuration
|
configuration: Configuration
|
||||||
) {
|
) {
|
||||||
cell.configure(
|
cell.configure(
|
||||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
|
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
delegate: configuration.userTableViewCellDelegate
|
delegate: configuration.userTableViewCellDelegate
|
||||||
|
@ -12,17 +12,13 @@ import MastodonSDK
|
|||||||
|
|
||||||
extension AppContext {
|
extension AppContext {
|
||||||
func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? {
|
func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? {
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
let accounts = AuthenticationServiceProvider.shared.authentications
|
||||||
guard
|
guard accounts.count > 1 else { return nil }
|
||||||
let accounts = try? managedObjectContext.fetch(request),
|
|
||||||
accounts.count > 1
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
let nextSelectedAccountIndex: Int? = {
|
let nextSelectedAccountIndex: Int? = {
|
||||||
for (index, account) in accounts.enumerated() {
|
for (index, account) in accounts.enumerated() {
|
||||||
guard account == authContext.mastodonAuthenticationBox
|
guard account == authContext.mastodonAuthenticationBox
|
||||||
.authenticationRecord
|
.authentication
|
||||||
.object(in: managedObjectContext)
|
|
||||||
else { continue }
|
else { continue }
|
||||||
|
|
||||||
let nextAccountIndex = index + 1
|
let nextAccountIndex = index + 1
|
||||||
|
@ -24,7 +24,7 @@ extension DataSourceFacade {
|
|||||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||||
|
|
||||||
try? await managedObjectContext.performChanges {
|
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 }
|
guard let user = record.object(in: managedObjectContext) else { return }
|
||||||
_ = Persistence.SearchHistory.createOrMerge(
|
_ = Persistence.SearchHistory.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
@ -42,7 +42,7 @@ extension DataSourceFacade {
|
|||||||
switch tag {
|
switch tag {
|
||||||
case .entity(let entity):
|
case .entity(let entity):
|
||||||
try? await managedObjectContext.performChanges {
|
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()
|
let now = Date()
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ extension DataSourceFacade {
|
|||||||
case .record(let record):
|
case .record(let record):
|
||||||
try? await managedObjectContext.performChanges {
|
try? await managedObjectContext.performChanges {
|
||||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
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 }
|
guard let tag = record.object(in: managedObjectContext) else { return }
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
@ -99,7 +99,7 @@ extension DataSourceFacade {
|
|||||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
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
|
let request = SearchHistory.sortedFetchRequest
|
||||||
request.predicate = SearchHistory.predicate(
|
request.predicate = SearchHistory.predicate(
|
||||||
domain: authenticationBox.domain,
|
domain: authenticationBox.domain,
|
||||||
|
@ -21,10 +21,8 @@ final class AccountListViewModel: NSObject {
|
|||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
|
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
|
|
||||||
@Published var items: [Item] = []
|
@Published var items: [Item] = []
|
||||||
|
|
||||||
let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
|
let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
|
||||||
@ -33,30 +31,11 @@ final class AccountListViewModel: NSObject {
|
|||||||
init(context: AppContext, authContext: AuthContext) {
|
init(context: AppContext, authContext: AuthContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.authContext = authContext
|
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()
|
super.init()
|
||||||
// end 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)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] authentications in
|
.sink { [weak self] authentications in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@ -85,7 +64,7 @@ extension AccountListViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
|
case authentication(record: MastodonAuthentication)
|
||||||
case addAccount
|
case addAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,12 +76,12 @@ extension AccountListViewModel {
|
|||||||
switch item {
|
switch item {
|
||||||
case .authentication(let record):
|
case .authentication(let record):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
|
||||||
if let authentication = record.object(in: managedObjectContext),
|
if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
|
||||||
let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
|
|
||||||
{
|
{
|
||||||
AccountListViewModel.configure(
|
AccountListViewModel.configure(
|
||||||
|
in: managedObjectContext,
|
||||||
cell: cell,
|
cell: cell,
|
||||||
authentication: authentication,
|
authentication: record,
|
||||||
activeAuthentication: activeAuthentication
|
activeAuthentication: activeAuthentication
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -119,11 +98,12 @@ extension AccountListViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
|
in context: NSManagedObjectContext,
|
||||||
cell: AccountListTableViewCell,
|
cell: AccountListTableViewCell,
|
||||||
authentication: MastodonAuthentication,
|
authentication: MastodonAuthentication,
|
||||||
activeAuthentication: MastodonAuthentication
|
activeAuthentication: MastodonAuthentication
|
||||||
) {
|
) {
|
||||||
let user = authentication.user
|
guard let user = authentication.user(in: context) else { return }
|
||||||
|
|
||||||
// avatar
|
// avatar
|
||||||
cell.avatarButton.avatarImageView.configure(
|
cell.avatarButton.avatarImageView.configure(
|
||||||
@ -168,16 +148,3 @@ extension AccountListViewModel {
|
|||||||
.joined(separator: ", ")
|
.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 } ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -66,8 +66,7 @@ extension AccountListViewController: PanModalPresentable {
|
|||||||
return .contentHeight(CGFloat(height))
|
return .contentHeight(CGFloat(height))
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
let authenticationCount = AuthenticationServiceProvider.shared.authentications.count
|
||||||
let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0
|
|
||||||
|
|
||||||
let count = authenticationCount + 1
|
let count = authenticationCount + 1
|
||||||
let height = calculateHeight(of: count)
|
let height = calculateHeight(of: count)
|
||||||
@ -165,9 +164,8 @@ extension AccountListViewController: UITableViewDelegate {
|
|||||||
switch item {
|
switch item {
|
||||||
case .authentication(let record):
|
case .authentication(let record):
|
||||||
assert(Thread.isMainThread)
|
assert(Thread.isMainThread)
|
||||||
guard let authentication = record.object(in: context.managedObjectContext) else { return }
|
|
||||||
Task { @MainActor in
|
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 }
|
guard isActive else { return }
|
||||||
self.coordinator.setup()
|
self.coordinator.setup()
|
||||||
} // end Task
|
} // end Task
|
||||||
|
@ -211,7 +211,8 @@ extension HomeTimelineViewController {
|
|||||||
|
|
||||||
let userDoesntFollowPeople: Bool
|
let userDoesntFollowPeople: Bool
|
||||||
if let managedObjectContext = self?.context.managedObjectContext,
|
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
|
userDoesntFollowPeople = me.followersCount == 0
|
||||||
} else {
|
} else {
|
||||||
userDoesntFollowPeople = true
|
userDoesntFollowPeople = true
|
||||||
|
@ -292,10 +292,9 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
|
|||||||
|
|
||||||
snapshot.appendSections([MastodonLoginViewSection.servers])
|
snapshot.appendSections([MastodonLoginViewSection.servers])
|
||||||
snapshot.appendItems(viewModel.filteredServers)
|
snapshot.appendItems(viewModel.filteredServers)
|
||||||
|
|
||||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.dataSource?.apply(snapshot, animatingDifferences: false)
|
||||||
let numberOfResults = viewModel.filteredServers.count
|
let numberOfResults = viewModel.filteredServers.count
|
||||||
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
||||||
}
|
}
|
||||||
|
@ -193,41 +193,26 @@ extension AuthenticationViewModel {
|
|||||||
domain: info.domain,
|
domain: info.domain,
|
||||||
authorization: authorization
|
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 account = response.value
|
||||||
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||||
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
|
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
|
||||||
mastodonUserRequest.fetchLimit = 1
|
mastodonUserRequest.fetchLimit = 1
|
||||||
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
|
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(
|
return response
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ final class FollowedTagsViewModel: NSObject {
|
|||||||
self.fetchedResultsController = FollowedTagsFetchedResultController(
|
self.fetchedResultsController = FollowedTagsFetchedResultController(
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
domain: authContext.mastodonAuthenticationBox.domain,
|
domain: authContext.mastodonAuthenticationBox.domain,
|
||||||
user: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)!.user
|
user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme:
|
||||||
)
|
)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -98,6 +98,7 @@ extension ProfileHeaderView.ViewModel {
|
|||||||
// follows you
|
// follows you
|
||||||
$relationshipActionOptionSet
|
$relationshipActionOptionSet
|
||||||
.map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
|
.map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { isFollowingBy in
|
.sink { isFollowingBy in
|
||||||
view.followsYouBlurEffectView.isHidden = !isFollowingBy
|
view.followsYouBlurEffectView.isHidden = !isFollowingBy
|
||||||
}
|
}
|
||||||
@ -182,16 +183,19 @@ extension ProfileHeaderView.ViewModel {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
$relationshipActionOptionSet
|
$relationshipActionOptionSet
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { optionSet in
|
.sink { optionSet in
|
||||||
let isBlocking = optionSet.contains(.blocking)
|
let isBlocking = optionSet.contains(.blocking)
|
||||||
let isBlockedBy = optionSet.contains(.blockingBy)
|
let isBlockedBy = optionSet.contains(.blockingBy)
|
||||||
let isSuspended = optionSet.contains(.suspended)
|
let isSuspended = optionSet.contains(.suspended)
|
||||||
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
|
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
|
||||||
|
|
||||||
view.bioMetaText.textView.isHidden = isNeedsHidden
|
view.bioMetaText.textView.isHidden = isNeedsHidden
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// dashboard
|
// dashboard
|
||||||
$isMyself
|
$isMyself
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { isMyself in
|
.sink { isMyself in
|
||||||
if isMyself {
|
if isMyself {
|
||||||
view.statusDashboardView.postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.myPosts
|
view.statusDashboardView.postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.myPosts
|
||||||
@ -246,6 +250,7 @@ extension ProfileHeaderView.ViewModel {
|
|||||||
$isEditing,
|
$isEditing,
|
||||||
$isUpdating
|
$isUpdating
|
||||||
)
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { relationshipActionOptionSet, isEditing, isUpdating in
|
.sink { relationshipActionOptionSet, isEditing, isUpdating in
|
||||||
if relationshipActionOptionSet.contains(.edit) {
|
if relationshipActionOptionSet.contains(.edit) {
|
||||||
// check .edit state and set .editing when isEditing
|
// check .edit state and set .editing when isEditing
|
||||||
|
@ -15,7 +15,7 @@ import MastodonSDK
|
|||||||
final class MeProfileViewModel: ProfileViewModel {
|
final class MeProfileViewModel: ProfileViewModel {
|
||||||
|
|
||||||
init(context: AppContext, authContext: AuthContext) {
|
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(
|
super.init(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
@ -29,5 +29,27 @@ final class MeProfileViewModel: ProfileViewModel {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,6 +273,8 @@ extension ProfileViewController {
|
|||||||
bindTitleView()
|
bindTitleView()
|
||||||
bindMoreBarButtonItem()
|
bindMoreBarButtonItem()
|
||||||
bindPager()
|
bindPager()
|
||||||
|
|
||||||
|
viewModel.viewDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
@ -935,11 +937,6 @@ extension ProfileViewController: PagerTabStripNavigateable {
|
|||||||
|
|
||||||
private extension ProfileViewController {
|
private extension ProfileViewController {
|
||||||
var currentInstance: Instance? {
|
var currentInstance: Instance? {
|
||||||
guard let authenticationRecord = authContext.mastodonAuthenticationBox
|
authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext)
|
||||||
.authenticationRecord
|
|
||||||
.object(in: context.managedObjectContext)
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
return authenticationRecord.instance
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ class ProfileViewModel: NSObject {
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// bind me
|
// bind me
|
||||||
self.me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
|
||||||
$me
|
$me
|
||||||
.assign(to: \.me, on: relationshipViewModel)
|
.assign(to: \.me, on: relationshipViewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
@ -171,9 +171,10 @@ class ProfileViewModel: NSObject {
|
|||||||
.assign(to: &$isPagingEnabled)
|
.assign(to: &$isPagingEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewModel {
|
func viewDidLoad() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// fetch profile info before edit
|
// fetch profile info before edit
|
||||||
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||||
|
@ -59,7 +59,7 @@ class ReportResultViewModel: ObservableObject {
|
|||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let user = user.object(in: context.managedObjectContext) else { return }
|
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.user = user
|
||||||
self.relationshipViewModel.me = me
|
self.relationshipViewModel.me = me
|
||||||
|
|
||||||
|
@ -258,28 +258,34 @@ extension MainTabBarController {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user {
|
NotificationCenter.default.publisher(for: .userFetched)
|
||||||
self.avatarURLObserver = user.publisher(for: \.avatar)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self, weak user] _ in
|
.sink { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let user = user else { return }
|
if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) {
|
||||||
guard user.managedObjectContext != nil else { return }
|
self.avatarURLObserver = user.publisher(for: \.avatar)
|
||||||
self.avatarURL = user.avatarImageURL()
|
.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
|
// a11y
|
||||||
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
|
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
|
||||||
guard let profileTabItem = _profileTabItem else { return }
|
guard let profileTabItem = _profileTabItem else { return }
|
||||||
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
|
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
|
||||||
|
|
||||||
context.authenticationService.updateActiveUserAccountPublisher
|
self.context.authenticationService.updateActiveUserAccountPublisher
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
self?.updateUserAccount()
|
self?.updateUserAccount()
|
||||||
|
}
|
||||||
|
.store(in: &self.disposeBag)
|
||||||
|
} else {
|
||||||
|
self.avatarURLObserver = nil
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
}
|
||||||
} else {
|
.store(in: &disposeBag)
|
||||||
self.avatarURLObserver = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
|
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
|
||||||
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
|
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
|
||||||
@ -451,9 +457,9 @@ extension MainTabBarController {
|
|||||||
authenticationBox: authContext.mastodonAuthenticationBox
|
authenticationBox: authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
|
||||||
if let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(
|
if let user = authContext.mastodonAuthenticationBox.authentication.user(
|
||||||
in: context.managedObjectContext
|
in: context.managedObjectContext
|
||||||
)?.user {
|
) {
|
||||||
user.update(
|
user.update(
|
||||||
property: .init(
|
property: .init(
|
||||||
entity: profileResponse.value,
|
entity: profileResponse.value,
|
||||||
|
@ -75,7 +75,7 @@ extension SidebarViewModel {
|
|||||||
let imageURL: URL? = {
|
let imageURL: URL? = {
|
||||||
switch item {
|
switch item {
|
||||||
case .me:
|
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()
|
return user?.avatarImageURL()
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
@ -132,7 +132,7 @@ extension SidebarViewModel {
|
|||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
case .me:
|
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
|
let currentUserDisplayName = user.displayNameWithFallback
|
||||||
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
|
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
|
||||||
default:
|
default:
|
||||||
|
@ -79,7 +79,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
|
|||||||
return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext(
|
return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext(
|
||||||
domain: authContext.mastodonAuthenticationBox.domain,
|
domain: authContext.mastodonAuthenticationBox.domain,
|
||||||
entity: status,
|
entity: status,
|
||||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user,
|
me: authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext),
|
||||||
statusCache: nil,
|
statusCache: nil,
|
||||||
userCache: nil,
|
userCache: nil,
|
||||||
networkDate: Date()))
|
networkDate: Date()))
|
||||||
|
@ -126,7 +126,7 @@ extension SearchResultSection {
|
|||||||
configuration: Configuration
|
configuration: Configuration
|
||||||
) {
|
) {
|
||||||
cell.configure(
|
cell.configure(
|
||||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
|
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
delegate: configuration.userTableViewCellDelegate
|
delegate: configuration.userTableViewCellDelegate
|
||||||
|
@ -17,6 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
let appContext = AppContext()
|
let appContext = AppContext()
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
AuthenticationServiceProvider.shared.restore()
|
||||||
|
|
||||||
AppSecret.default.register()
|
AppSecret.default.register()
|
||||||
|
|
||||||
|
@ -185,11 +185,8 @@ extension SceneDelegate {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
|
||||||
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
|
guard let authentication = AuthenticationServiceProvider.shared.getAuthentication(matching: accessToken) else {
|
||||||
request.fetchLimit = 1
|
|
||||||
|
|
||||||
guard let authentication = try? coordinator.appContext.managedObjectContext.fetch(request).first else {
|
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -50,9 +50,8 @@ extension SendPostIntentHandler: SendPostIntentHandling {
|
|||||||
let mastodonAuthentications: [MastodonAuthentication]
|
let mastodonAuthentications: [MastodonAuthentication]
|
||||||
let accounts = intent.accounts ?? []
|
let accounts = intent.accounts ?? []
|
||||||
if accounts.isEmpty {
|
if accounts.isEmpty {
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
// fixme: refactor this and implemented method on AuthenticationServiceProvider
|
||||||
let authentications = try managedObjectContext.fetch(request)
|
let _authentication = AuthenticationServiceProvider.shared.authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
|
||||||
let _authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
|
|
||||||
|
|
||||||
guard let authentication = _authentication else {
|
guard let authentication = _authentication else {
|
||||||
let failureReason = APIService.APIError.implicit(.authenticationMissing).errorDescription ?? "Fail to Send Post"
|
let failureReason = APIService.APIError.implicit(.authenticationMissing).errorDescription ?? "Fail to Send Post"
|
||||||
@ -65,12 +64,12 @@ extension SendPostIntentHandler: SendPostIntentHandling {
|
|||||||
|
|
||||||
let authenticationBoxes = mastodonAuthentications.map { authentication in
|
let authenticationBoxes = mastodonAuthentications.map { authentication in
|
||||||
MastodonAuthenticationBox(
|
MastodonAuthenticationBox(
|
||||||
authenticationRecord: .init(objectID: authentication.objectID),
|
authentication: authentication,
|
||||||
domain: authentication.domain,
|
domain: authentication.domain,
|
||||||
userID: authentication.userID,
|
userID: authentication.userID,
|
||||||
appAuthorization: .init(accessToken: authentication.appAccessToken),
|
appAuthorization: .init(accessToken: authentication.appAccessToken),
|
||||||
userAuthorization: .init(accessToken: authentication.userAccessToken),
|
userAuthorization: .init(accessToken: authentication.userAccessToken),
|
||||||
inMemoryCache: .sharedCache(for: authentication.objectID.description)
|
inMemoryCache: .sharedCache(for: authentication.userAccessToken)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,9 +17,11 @@ extension Account {
|
|||||||
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
|
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
|
||||||
// get accounts
|
// get accounts
|
||||||
let accounts: [Account] = try await managedObjectContext.perform {
|
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 accounts = results.compactMap { mastodonAuthentication -> Account? in
|
||||||
let user = mastodonAuthentication.user
|
guard let user = mastodonAuthentication.user(in: managedObjectContext) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let account = Account(
|
let account = Account(
|
||||||
identifier: mastodonAuthentication.identifier.uuidString,
|
identifier: mastodonAuthentication.identifier.uuidString,
|
||||||
display: user.displayNameWithFallback,
|
display: user.displayNameWithFallback,
|
||||||
@ -43,9 +45,7 @@ extension Array where Element == Account {
|
|||||||
let identifiers = self
|
let identifiers = self
|
||||||
.compactMap { $0.identifier }
|
.compactMap { $0.identifier }
|
||||||
.compactMap { UUID(uuidString: $0) }
|
.compactMap { UUID(uuidString: $0) }
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
let results = AuthenticationServiceProvider.shared.authentications.filter({ identifiers.contains($0.identifier) })
|
||||||
request.predicate = MastodonAuthentication.predicate(identifiers: identifiers)
|
|
||||||
let results = try managedObjectContext.fetch(request)
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -65,7 +65,7 @@
|
|||||||
<attribute name="version" optional="YES" attributeType="String"/>
|
<attribute name="version" optional="YES" attributeType="String"/>
|
||||||
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
||||||
</entity>
|
</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="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="appAccessToken" attributeType="String"/>
|
<attribute name="appAccessToken" attributeType="String"/>
|
||||||
<attribute name="clientID" attributeType="String"/>
|
<attribute name="clientID" attributeType="String"/>
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
<attribute name="version" optional="YES" attributeType="String"/>
|
<attribute name="version" optional="YES" attributeType="String"/>
|
||||||
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
||||||
</entity>
|
</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="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="appAccessToken" attributeType="String"/>
|
<attribute name="appAccessToken" attributeType="String"/>
|
||||||
<attribute name="clientID" attributeType="String"/>
|
<attribute name="clientID" attributeType="String"/>
|
||||||
|
@ -20,9 +20,14 @@ public final class CoreDataStack {
|
|||||||
self.storeDescriptions = storeDescriptions
|
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 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])
|
self.init(persistentStoreDescriptions: [storeDescription])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,16 +120,18 @@ extension CoreDataStack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CoreDataStack {
|
public extension CoreDataStack {
|
||||||
|
func tearDown() {
|
||||||
public func rebuild() {
|
|
||||||
let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!)
|
let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!)
|
||||||
try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil)
|
try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebuild() {
|
||||||
|
tearDown()
|
||||||
|
|
||||||
CoreDataStack.load(persistentContainer: persistentContainer) { [weak self] in
|
CoreDataStack.load(persistentContainer: persistentContainer) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.didFinishLoad.value = true
|
self.didFinishLoad.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ public final class Instance: NSManagedObject {
|
|||||||
@NSManaged public private(set) var configurationV2Raw: Data?
|
@NSManaged public private(set) var configurationV2Raw: Data?
|
||||||
|
|
||||||
// MARK: one-to-many relationships
|
// MARK: one-to-many relationships
|
||||||
@NSManaged public var authentications: Set<MastodonAuthentication>
|
@NSManaged public var authentications: Set<MastodonAuthenticationLegacy>
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
final public class MastodonAuthentication: NSManagedObject {
|
@objc(MastodonAuthentication)
|
||||||
|
final public class MastodonAuthenticationLegacy: NSManagedObject {
|
||||||
|
|
||||||
public typealias ID = UUID
|
public typealias ID = UUID
|
||||||
|
|
||||||
@ -35,16 +36,16 @@ final public class MastodonAuthentication: NSManagedObject {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonAuthentication {
|
extension MastodonAuthenticationLegacy {
|
||||||
|
|
||||||
public override func awakeFromInsert() {
|
public override func awakeFromInsert() {
|
||||||
super.awakeFromInsert()
|
super.awakeFromInsert()
|
||||||
|
|
||||||
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthentication.identifier))
|
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthenticationLegacy.identifier))
|
||||||
let now = Date()
|
let now = Date()
|
||||||
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.createdAt))
|
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.createdAt))
|
||||||
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.updatedAt))
|
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.updatedAt))
|
||||||
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.activedAt))
|
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.activedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@ -52,8 +53,8 @@ extension MastodonAuthentication {
|
|||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property,
|
property: Property,
|
||||||
user: MastodonUser
|
user: MastodonUser
|
||||||
) -> MastodonAuthentication {
|
) -> MastodonAuthenticationLegacy {
|
||||||
let authentication: MastodonAuthentication = context.insertObject()
|
let authentication: MastodonAuthenticationLegacy = context.insertObject()
|
||||||
|
|
||||||
authentication.domain = property.domain
|
authentication.domain = property.domain
|
||||||
authentication.userID = property.userID
|
authentication.userID = property.userID
|
||||||
@ -112,7 +113,7 @@ extension MastodonAuthentication {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonAuthentication {
|
extension MastodonAuthenticationLegacy {
|
||||||
public struct Property {
|
public struct Property {
|
||||||
|
|
||||||
public let domain: String
|
public let domain: String
|
||||||
@ -144,51 +145,51 @@ extension MastodonAuthentication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonAuthentication: Managed {
|
extension MastodonAuthenticationLegacy: Managed {
|
||||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
return [NSSortDescriptor(keyPath: \MastodonAuthentication.createdAt, ascending: false)]
|
return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.createdAt, ascending: false)]
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var activeSortDescriptors: [NSSortDescriptor] {
|
public static var activeSortDescriptors: [NSSortDescriptor] {
|
||||||
return [NSSortDescriptor(keyPath: \MastodonAuthentication.activedAt, ascending: false)]
|
return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.activedAt, ascending: false)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonAuthentication {
|
extension MastodonAuthenticationLegacy {
|
||||||
public static var activeSortedFetchRequest: NSFetchRequest<MastodonAuthentication> {
|
public static var activeSortedFetchRequest: NSFetchRequest<MastodonAuthenticationLegacy> {
|
||||||
let request = NSFetchRequest<MastodonAuthentication>(entityName: entityName)
|
let request = NSFetchRequest<MastodonAuthenticationLegacy>(entityName: entityName)
|
||||||
request.sortDescriptors = activeSortDescriptors
|
request.sortDescriptors = activeSortDescriptors
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonAuthentication {
|
extension MastodonAuthenticationLegacy {
|
||||||
|
|
||||||
public static func predicate(domain: String) -> NSPredicate {
|
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 {
|
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 {
|
public static func predicate(domain: String, userID: String) -> NSPredicate {
|
||||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
MastodonAuthentication.predicate(domain: domain),
|
MastodonAuthenticationLegacy.predicate(domain: domain),
|
||||||
MastodonAuthentication.predicate(userID: userID)
|
MastodonAuthenticationLegacy.predicate(userID: userID)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func predicate(userAccessToken: String) -> NSPredicate {
|
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 {
|
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 {
|
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])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ final public class MastodonUser: NSManagedObject {
|
|||||||
|
|
||||||
// one-to-one relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var pinnedStatus: Status?
|
@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
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var statuses: Set<Status>
|
@NSManaged public private(set) var statuses: Set<Status>
|
||||||
|
@ -46,9 +46,18 @@ public class AppContext: ObservableObject {
|
|||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
|
|
||||||
|
let authProvider = AuthenticationServiceProvider.shared
|
||||||
let _coreDataStack = CoreDataStack()
|
let _coreDataStack = CoreDataStack()
|
||||||
|
if authProvider.authenticationMigrationRequired {
|
||||||
|
authProvider.migrateLegacyAuthentications(
|
||||||
|
in: _coreDataStack.persistentContainer.viewContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
|
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
|
||||||
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()
|
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()
|
||||||
|
|
||||||
coreDataStack = _coreDataStack
|
coreDataStack = _coreDataStack
|
||||||
managedObjectContext = _managedObjectContext
|
managedObjectContext = _managedObjectContext
|
||||||
backgroundManagedObjectContext = _backgroundManagedObjectContext
|
backgroundManagedObjectContext = _backgroundManagedObjectContext
|
||||||
|
@ -24,31 +24,12 @@ public class AuthContext {
|
|||||||
private init(mastodonAuthenticationBox: MastodonAuthenticationBox) {
|
private init(mastodonAuthenticationBox: MastodonAuthenticationBox) {
|
||||||
self.mastodonAuthenticationBox = mastodonAuthenticationBox
|
self.mastodonAuthenticationBox = mastodonAuthenticationBox
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AuthContext {
|
extension AuthContext {
|
||||||
|
|
||||||
public convenience init?(authentication: MastodonAuthentication) {
|
public convenience init?(authentication: MastodonAuthentication) {
|
||||||
self.init(mastodonAuthenticationBox: MastodonAuthenticationBox(authentication: authentication))
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import CoreDataStack
|
|||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
public struct MastodonAuthenticationBox: UserIdentifier {
|
public struct MastodonAuthenticationBox: UserIdentifier {
|
||||||
public let authenticationRecord: ManagedObjectRecord<MastodonAuthentication>
|
public let authentication: MastodonAuthentication
|
||||||
public let domain: String
|
public let domain: String
|
||||||
public let userID: MastodonUser.ID
|
public let userID: MastodonUser.ID
|
||||||
public let appAuthorization: Mastodon.API.OAuth.Authorization
|
public let appAuthorization: Mastodon.API.OAuth.Authorization
|
||||||
@ -18,14 +18,14 @@ public struct MastodonAuthenticationBox: UserIdentifier {
|
|||||||
public let inMemoryCache: MastodonAccountInMemoryCache
|
public let inMemoryCache: MastodonAccountInMemoryCache
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
authenticationRecord: ManagedObjectRecord<MastodonAuthentication>,
|
authentication: MastodonAuthentication,
|
||||||
domain: String,
|
domain: String,
|
||||||
userID: MastodonUser.ID,
|
userID: MastodonUser.ID,
|
||||||
appAuthorization: Mastodon.API.OAuth.Authorization,
|
appAuthorization: Mastodon.API.OAuth.Authorization,
|
||||||
userAuthorization: Mastodon.API.OAuth.Authorization,
|
userAuthorization: Mastodon.API.OAuth.Authorization,
|
||||||
inMemoryCache: MastodonAccountInMemoryCache
|
inMemoryCache: MastodonAccountInMemoryCache
|
||||||
) {
|
) {
|
||||||
self.authenticationRecord = authenticationRecord
|
self.authentication = authentication
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.userID = userID
|
self.userID = userID
|
||||||
self.appAuthorization = appAuthorization
|
self.appAuthorization = appAuthorization
|
||||||
@ -38,12 +38,12 @@ extension MastodonAuthenticationBox {
|
|||||||
|
|
||||||
init(authentication: MastodonAuthentication) {
|
init(authentication: MastodonAuthentication) {
|
||||||
self = MastodonAuthenticationBox(
|
self = MastodonAuthenticationBox(
|
||||||
authenticationRecord: .init(objectID: authentication.objectID),
|
authentication: authentication,
|
||||||
domain: authentication.domain,
|
domain: authentication.domain,
|
||||||
userID: authentication.userID,
|
userID: authentication.userID,
|
||||||
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
|
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
|
||||||
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken),
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift
Normal file
107
MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -167,7 +167,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
_ = Persistence.Tag.createOrMerge(
|
_ = Persistence.Tag.createOrMerge(
|
||||||
|
@ -67,12 +67,15 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
|
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
|
||||||
guard let user = user.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let user = user.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else {
|
else {
|
||||||
throw APIError.implicit(.badRequest)
|
throw APIError.implicit(.badRequest)
|
||||||
}
|
}
|
||||||
let me = authentication.user
|
|
||||||
let isBlocking = user.blockingBy.contains(me)
|
let isBlocking = user.blockingBy.contains(me)
|
||||||
let isFollowing = user.followingBy.contains(me)
|
let isFollowing = user.followingBy.contains(me)
|
||||||
// toggle block state
|
// toggle block state
|
||||||
@ -116,10 +119,13 @@ extension APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let user = user.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let user = user.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else { return }
|
else { return }
|
||||||
let me = authentication.user
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
|
@ -27,12 +27,15 @@ extension APIService {
|
|||||||
|
|
||||||
// update bookmark state and retrieve bookmark context
|
// update bookmark state and retrieve bookmark context
|
||||||
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges {
|
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges {
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let _status = record.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let _status = record.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else {
|
else {
|
||||||
throw APIError.implicit(.badRequest)
|
throw APIError.implicit(.badRequest)
|
||||||
}
|
}
|
||||||
let me = authentication.user
|
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
let isBookmarked = status.bookmarkedBy.contains(me)
|
let isBookmarked = status.bookmarkedBy.contains(me)
|
||||||
status.update(bookmarked: !isBookmarked, by: me)
|
status.update(bookmarked: !isBookmarked, by: me)
|
||||||
@ -60,10 +63,13 @@ extension APIService {
|
|||||||
|
|
||||||
// update bookmark state
|
// update bookmark state
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let _status = record.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let _status = record.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else { return }
|
else { return }
|
||||||
let me = authentication.user
|
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
@ -108,7 +114,10 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
|
|
||||||
|
guard
|
||||||
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,15 @@ extension APIService {
|
|||||||
|
|
||||||
// update like state and retrieve like context
|
// update like state and retrieve like context
|
||||||
let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges {
|
let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges {
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let _status = record.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let _status = record.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else {
|
else {
|
||||||
throw APIError.implicit(.badRequest)
|
throw APIError.implicit(.badRequest)
|
||||||
}
|
}
|
||||||
let me = authentication.user
|
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
let isFavorited = status.favouritedBy.contains(me)
|
let isFavorited = status.favouritedBy.contains(me)
|
||||||
let favoritedCount = status.favouritesCount
|
let favoritedCount = status.favouritesCount
|
||||||
@ -65,10 +68,13 @@ extension APIService {
|
|||||||
|
|
||||||
// update like state
|
// update like state
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let _status = record.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let _status = record.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else { return }
|
else { return }
|
||||||
let me = authentication.user
|
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
@ -117,7 +123,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
|
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
|
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 }
|
guard let user = user.object(in: managedObjectContext) else { return nil }
|
||||||
|
|
||||||
let isFollowing = user.followingBy.contains(me)
|
let isFollowing = user.followingBy.contains(me)
|
||||||
@ -88,7 +88,7 @@ extension APIService {
|
|||||||
|
|
||||||
// update friendship state
|
// update friendship state
|
||||||
try await managedObjectContext.performChanges {
|
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)
|
let user = user.object(in: managedObjectContext)
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
@ -120,10 +120,9 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
guard let user = user.object(in: managedObjectContext),
|
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) }
|
else { throw APIError.implicit(.badRequest) }
|
||||||
|
|
||||||
let me = authentication.user
|
|
||||||
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
|
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
|
||||||
|
|
||||||
let oldShowReblogs = me.showingReblogsBy.contains(user)
|
let oldShowReblogs = me.showingReblogsBy.contains(user)
|
||||||
@ -144,7 +143,7 @@ extension APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
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 {
|
switch result {
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
|
@ -35,7 +35,7 @@ extension APIService {
|
|||||||
)
|
)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
guard let user = managedObjectContext.safeFetch(request).first else { return }
|
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(
|
Persistence.MastodonUser.update(
|
||||||
mastodonUser: user,
|
mastodonUser: user,
|
||||||
|
@ -35,7 +35,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
let result = Persistence.MastodonUser.createOrMerge(
|
let result = Persistence.MastodonUser.createOrMerge(
|
||||||
|
@ -36,8 +36,8 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
let result = Persistence.MastodonUser.createOrMerge(
|
let result = Persistence.MastodonUser.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
|
@ -44,8 +44,8 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
_ = Persistence.Status.createOrMerge(
|
_ = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
|
@ -11,6 +11,10 @@ import CoreData
|
|||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
|
public extension Foundation.Notification.Name {
|
||||||
|
static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched")
|
||||||
|
}
|
||||||
|
|
||||||
extension APIService {
|
extension APIService {
|
||||||
|
|
||||||
public func homeTimeline(
|
public func homeTimeline(
|
||||||
@ -38,9 +42,21 @@ extension APIService {
|
|||||||
).singleOutput()
|
).singleOutput()
|
||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
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 {
|
try await managedObjectContext.performChanges {
|
||||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
|
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,13 +66,15 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges {
|
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges {
|
||||||
guard let user = user.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let user = user.object(in: managedObjectContext),
|
||||||
|
let me = authentication.user(in: managedObjectContext)
|
||||||
else {
|
else {
|
||||||
throw APIError.implicit(.badRequest)
|
throw APIError.implicit(.badRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
let me = authentication.user
|
|
||||||
let isMuting = user.mutingBy.contains(me)
|
let isMuting = user.mutingBy.contains(me)
|
||||||
|
|
||||||
// toggle mute state
|
// toggle mute state
|
||||||
@ -112,9 +114,8 @@ extension APIService {
|
|||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let user = user.object(in: managedObjectContext),
|
guard let user = user.object(in: managedObjectContext),
|
||||||
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
else { return }
|
else { return }
|
||||||
let me = authentication.user
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
|
@ -88,7 +88,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
|
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -176,7 +176,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
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(
|
_ = Persistence.Notification.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Notification.PersistContext(
|
context: Persistence.Notification.PersistContext(
|
||||||
|
@ -35,7 +35,7 @@ extension APIService {
|
|||||||
).singleOutput()
|
).singleOutput()
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
_ = Persistence.Poll.createOrMerge(
|
_ = Persistence.Poll.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Poll.PersistContext(
|
context: Persistence.Poll.PersistContext(
|
||||||
@ -78,7 +78,7 @@ extension APIService {
|
|||||||
).singleOutput()
|
).singleOutput()
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
_ = Persistence.Poll.createOrMerge(
|
_ = Persistence.Poll.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Poll.PersistContext(
|
context: Persistence.Poll.PersistContext(
|
||||||
|
@ -29,7 +29,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
_ = Persistence.Status.createOrMerge(
|
_ = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
|
@ -27,11 +27,13 @@ extension APIService {
|
|||||||
|
|
||||||
// update repost state and retrieve repost context
|
// update repost state and retrieve repost context
|
||||||
let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges {
|
let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges {
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let _status = record.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let me = authentication.user(in: managedObjectContext),
|
||||||
|
let _status = record.object(in: managedObjectContext)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
let me = authentication.user
|
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
let isReblogged = status.rebloggedBy.contains(me)
|
let isReblogged = status.rebloggedBy.contains(me)
|
||||||
let rebloggedCount = status.reblogsCount
|
let rebloggedCount = status.reblogsCount
|
||||||
@ -66,10 +68,13 @@ extension APIService {
|
|||||||
|
|
||||||
// update repost state
|
// update repost state
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext),
|
let authentication = authenticationBox.authentication
|
||||||
let _status = record.object(in: managedObjectContext)
|
|
||||||
|
guard
|
||||||
|
let me = authentication.user(in: managedObjectContext),
|
||||||
|
let _status = record.object(in: managedObjectContext)
|
||||||
else { return }
|
else { return }
|
||||||
let me = authentication.user
|
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -41,7 +41,7 @@ extension APIService {
|
|||||||
).singleOutput()
|
).singleOutput()
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
|
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
|
||||||
// assertionFailure()
|
// assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
|
||||||
// user
|
// user
|
||||||
for entity in response.value.accounts {
|
for entity in response.value.accounts {
|
||||||
|
@ -76,7 +76,7 @@ extension APIService {
|
|||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
let status = Persistence.Status.createOrMerge(
|
let status = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Status.PersistContext(
|
context: Persistence.Status.PersistContext(
|
||||||
|
@ -33,7 +33,7 @@ extension APIService {
|
|||||||
#if !APP_EXTENSION
|
#if !APP_EXTENSION
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
_ = Persistence.Status.createOrMerge(
|
_ = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Status.PersistContext(
|
context: Persistence.Status.PersistContext(
|
||||||
|
@ -29,7 +29,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
_ = Persistence.Status.createOrMerge(
|
_ = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Status.PersistContext(
|
context: Persistence.Status.PersistContext(
|
||||||
|
@ -73,7 +73,7 @@ fileprivate extension APIService {
|
|||||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> {
|
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> {
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
|
|
||||||
_ = Persistence.Tag.createOrMerge(
|
_ = Persistence.Tag.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
|
@ -29,7 +29,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
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
|
let value = response.value.ancestors + response.value.descendants
|
||||||
|
|
||||||
for entity in value {
|
for entity in value {
|
||||||
|
@ -45,7 +45,7 @@ extension APIService {
|
|||||||
|
|
||||||
let managedObjectContext = self.backgroundManagedObjectContext
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
_ = Persistence.Status.createOrMerge(
|
_ = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -21,10 +21,9 @@ public final class AuthenticationService: NSObject {
|
|||||||
weak var apiService: APIService?
|
weak var apiService: APIService?
|
||||||
let managedObjectContext: NSManagedObjectContext // read-only
|
let managedObjectContext: NSManagedObjectContext // read-only
|
||||||
let backgroundManagedObjectContext: NSManagedObjectContext
|
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||||
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
|
let authenticationServiceProvider = AuthenticationServiceProvider.shared
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@Published public var mastodonAuthentications: [ManagedObjectRecord<MastodonAuthentication>] = []
|
|
||||||
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
|
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
|
||||||
|
|
||||||
private func fetchFollowedBlockedUserIds(
|
private func fetchFollowedBlockedUserIds(
|
||||||
@ -92,21 +91,8 @@ public final class AuthenticationService: NSObject {
|
|||||||
self.managedObjectContext = managedObjectContext
|
self.managedObjectContext = managedObjectContext
|
||||||
self.backgroundManagedObjectContext = backgroundManagedObjectContext
|
self.backgroundManagedObjectContext = backgroundManagedObjectContext
|
||||||
self.apiService = apiService
|
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
|
$mastodonAuthenticationBoxes
|
||||||
.sink { [weak self] boxes in
|
.sink { [weak self] boxes in
|
||||||
@ -122,10 +108,9 @@ public final class AuthenticationService: NSObject {
|
|||||||
|
|
||||||
// TODO: verify credentials for active authentication
|
// TODO: verify credentials for active authentication
|
||||||
|
|
||||||
$mastodonAuthentications
|
authenticationServiceProvider.$authentications
|
||||||
.map { authentications -> [MastodonAuthenticationBox] in
|
.map { authentications -> [MastodonAuthenticationBox] in
|
||||||
return authentications
|
return authentications
|
||||||
.compactMap { $0.object(in: managedObjectContext) }
|
|
||||||
.sorted(by: { $0.activedAt > $1.activedAt })
|
.sorted(by: { $0.activedAt > $1.activedAt })
|
||||||
.compactMap { authentication -> MastodonAuthenticationBox? in
|
.compactMap { authentication -> MastodonAuthenticationBox? in
|
||||||
return MastodonAuthenticationBox(authentication: authentication)
|
return MastodonAuthenticationBox(authentication: authentication)
|
||||||
@ -133,14 +118,7 @@ public final class AuthenticationService: NSObject {
|
|||||||
}
|
}
|
||||||
.assign(to: &$mastodonAuthenticationBoxes)
|
.assign(to: &$mastodonAuthenticationBoxes)
|
||||||
|
|
||||||
do {
|
AuthenticationServiceProvider.shared.authentications = AuthenticationServiceProvider.shared.authenticationSortedByActivation()
|
||||||
try mastodonAuthenticationFetchedResultsController.performFetch()
|
|
||||||
mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?
|
|
||||||
.sorted(by: { $0.activedAt > $1.activedAt })
|
|
||||||
.compactMap { $0.asRecord } ?? []
|
|
||||||
} catch {
|
|
||||||
assertionFailure(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -150,18 +128,9 @@ extension AuthenticationService {
|
|||||||
public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool {
|
public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool {
|
||||||
var isActive = false
|
var isActive = false
|
||||||
|
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID)
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
isActive = true
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return isActive
|
return isActive
|
||||||
}
|
}
|
||||||
@ -182,12 +151,7 @@ extension AuthenticationService {
|
|||||||
managedObjectContext.delete(feed)
|
managedObjectContext.delete(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) else {
|
AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication)
|
||||||
assertionFailure()
|
|
||||||
throw APIService.APIError.implicit(.authenticationMissing)
|
|
||||||
}
|
|
||||||
|
|
||||||
managedObjectContext.delete(authentication)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel push notification subscription
|
// 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 } ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -78,18 +78,8 @@ extension InstanceService {
|
|||||||
networkDate: response.networkDate
|
networkDate: response.networkDate
|
||||||
)
|
)
|
||||||
|
|
||||||
// update relationship
|
// update instance
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.tryMap { result in
|
.tryMap { result in
|
||||||
@ -116,18 +106,8 @@ extension InstanceService {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// update relationship
|
// update instance
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.tryMap { result in
|
.tryMap { result in
|
||||||
|
@ -41,7 +41,7 @@ public final class NotificationService {
|
|||||||
self.apiService = apiService
|
self.apiService = apiService
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
|
|
||||||
authenticationService.$mastodonAuthentications
|
AuthenticationServiceProvider.shared.$authentications
|
||||||
.sink(receiveValue: { [weak self] mastodonAuthentications in
|
.sink(receiveValue: { [weak self] mastodonAuthentications in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@ -100,13 +100,13 @@ extension NotificationService {
|
|||||||
let managedObjectContext = authenticationService.managedObjectContext
|
let managedObjectContext = authenticationService.managedObjectContext
|
||||||
return try await managedObjectContext.perform {
|
return try await managedObjectContext.perform {
|
||||||
var items: [UIApplicationShortcutItem] = []
|
var items: [UIApplicationShortcutItem] = []
|
||||||
for object in authenticationService.mastodonAuthentications {
|
for authentication in AuthenticationServiceProvider.shared.authentications {
|
||||||
guard let authentication = managedObjectContext.object(with: object.objectID) as? MastodonAuthentication else { continue }
|
guard let user = authentication.user(in: managedObjectContext) else { continue }
|
||||||
let accessToken = authentication.userAccessToken
|
let accessToken = authentication.userAccessToken
|
||||||
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
|
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
|
||||||
guard count > 0 else { continue }
|
guard count > 0 else { continue }
|
||||||
|
|
||||||
let title = "@\(authentication.user.acctWithDomain)"
|
let title = "@\(user.acctWithDomain)"
|
||||||
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
|
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
|
||||||
|
|
||||||
let item = UIApplicationShortcutItem(
|
let item = UIApplicationShortcutItem(
|
||||||
@ -201,9 +201,8 @@ extension NotificationService {
|
|||||||
|
|
||||||
let needsCancelSubscription: Bool = try await managedObjectContext.perform {
|
let needsCancelSubscription: Bool = try await managedObjectContext.perform {
|
||||||
// check authentication exists
|
// check authentication exists
|
||||||
let authenticationRequest = MastodonAuthentication.sortedFetchRequest
|
let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == userAccessToken }
|
||||||
authenticationRequest.predicate = MastodonAuthentication.predicate(userAccessToken: userAccessToken)
|
return results.first == nil
|
||||||
return managedObjectContext.safeFetch(authenticationRequest).first == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard needsCancelSubscription else {
|
guard needsCancelSubscription else {
|
||||||
@ -240,22 +239,17 @@ extension NotificationService {
|
|||||||
|
|
||||||
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
|
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
|
||||||
guard let authenticationService = self.authenticationService else { return nil }
|
guard let authenticationService = self.authenticationService else { return nil }
|
||||||
let managedObjectContext = authenticationService.managedObjectContext
|
let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken }
|
||||||
return try await managedObjectContext.perform {
|
guard let authentication = results.first else { return nil }
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
|
||||||
request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken)
|
return MastodonAuthenticationBox(
|
||||||
request.fetchLimit = 1
|
authentication: authentication,
|
||||||
guard let authentication = managedObjectContext.safeFetch(request).first else { return nil }
|
domain: authentication.domain,
|
||||||
|
userID: authentication.userID,
|
||||||
return MastodonAuthenticationBox(
|
appAuthorization: .init(accessToken: authentication.appAccessToken),
|
||||||
authenticationRecord: .init(objectID: authentication.objectID),
|
userAuthorization: .init(accessToken: authentication.userAccessToken),
|
||||||
domain: authentication.domain,
|
inMemoryCache: .sharedCache(for: authentication.userAccessToken)
|
||||||
userID: authentication.userID,
|
)
|
||||||
appAuthorization: .init(accessToken: authentication.appAccessToken),
|
|
||||||
userAuthorization: .init(accessToken: authentication.userAccessToken),
|
|
||||||
inMemoryCache: .sharedCache(for: authentication.objectID.description)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -156,7 +156,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||||||
self.visibility = {
|
self.visibility = {
|
||||||
// default private when user locked
|
// default private when user locked
|
||||||
var visibility: Mastodon.Entity.Status.Visibility = {
|
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 .public
|
||||||
}
|
}
|
||||||
return author.locked ? .private : .public
|
return author.locked ? .private : .public
|
||||||
@ -224,7 +224,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
|
||||||
|
|
||||||
var mentionAccts: [String] = []
|
var mentionAccts: [String] = []
|
||||||
if author?.id != status.author.id {
|
if author?.id != status.author.id {
|
||||||
@ -259,9 +259,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||||||
let _configuration: Mastodon.Entity.Instance.Configuration? = {
|
let _configuration: Mastodon.Entity.Instance.Configuration? = {
|
||||||
var configuration: Mastodon.Entity.Instance.Configuration? = nil
|
var configuration: Mastodon.Entity.Instance.Configuration? = nil
|
||||||
context.managedObjectContext.performAndWait {
|
context.managedObjectContext.performAndWait {
|
||||||
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
|
let authentication = authContext.mastodonAuthenticationBox.authentication
|
||||||
else { return }
|
configuration = authentication.instance(in: context.managedObjectContext)?.configuration
|
||||||
configuration = authentication.instance?.configuration
|
|
||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
}()
|
}()
|
||||||
@ -319,7 +318,7 @@ extension ComposeContentViewModel {
|
|||||||
$authContext
|
$authContext
|
||||||
.sink { [weak self] authContext in
|
.sink { [weak self] authContext in
|
||||||
guard let self = self else { return }
|
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.avatarURL = user.avatarImageURL()
|
||||||
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
|
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||||
self.username = user.acctWithDomain
|
self.username = user.acctWithDomain
|
||||||
@ -565,7 +564,7 @@ extension ComposeContentViewModel {
|
|||||||
let managedObjectContext = self.context.managedObjectContext
|
let managedObjectContext = self.context.managedObjectContext
|
||||||
var _author: ManagedObjectRecord<MastodonUser>?
|
var _author: ManagedObjectRecord<MastodonUser>?
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord
|
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
|
||||||
}
|
}
|
||||||
guard let author = _author else {
|
guard let author = _author else {
|
||||||
throw AppError.badAuthentication
|
throw AppError.badAuthentication
|
||||||
@ -621,7 +620,7 @@ extension ComposeContentViewModel {
|
|||||||
let managedObjectContext = self.context.managedObjectContext
|
let managedObjectContext = self.context.managedObjectContext
|
||||||
var _author: ManagedObjectRecord<MastodonUser>?
|
var _author: ManagedObjectRecord<MastodonUser>?
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord
|
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
|
||||||
}
|
}
|
||||||
guard let author = _author else {
|
guard let author = _author else {
|
||||||
throw AppError.badAuthentication
|
throw AppError.badAuthentication
|
||||||
|
@ -237,9 +237,8 @@ extension NotificationView.ViewModel {
|
|||||||
|
|
||||||
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
|
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
|
||||||
context.managedObjectContext.performAndWait {
|
context.managedObjectContext.performAndWait {
|
||||||
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
|
let authentication = authContext.mastodonAuthenticationBox.authentication
|
||||||
else { return }
|
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
|
||||||
configuration = authentication.instance?.configurationV2
|
|
||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
}()
|
}()
|
||||||
|
@ -686,9 +686,8 @@ extension StatusView.ViewModel {
|
|||||||
|
|
||||||
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
|
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
|
||||||
context.managedObjectContext.performAndWait {
|
context.managedObjectContext.performAndWait {
|
||||||
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
|
let authentication = authContext.mastodonAuthenticationBox.authentication
|
||||||
else { return }
|
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
|
||||||
configuration = authentication.instance?.configurationV2
|
|
||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
}()
|
}()
|
||||||
|
@ -126,6 +126,7 @@ public final class RelationshipViewModel {
|
|||||||
$me,
|
$me,
|
||||||
relationshipUpdatePublisher
|
relationshipUpdatePublisher
|
||||||
)
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] user, me, _ in
|
.sink { [weak self] user, me, _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.update(user: user, me: me)
|
self.update(user: user, me: me)
|
||||||
|
@ -160,8 +160,7 @@ extension ShareViewController {
|
|||||||
|
|
||||||
extension ShareViewController {
|
extension ShareViewController {
|
||||||
private func setupAuthContext() throws -> AuthContext? {
|
private func setupAuthContext() throws -> AuthContext? {
|
||||||
let request = MastodonAuthentication.activeSortedFetchRequest // use active order
|
let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
|
||||||
let _authentication = try context.managedObjectContext.fetch(request).first
|
|
||||||
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
||||||
return _authContext
|
return _authContext
|
||||||
}
|
}
|
||||||
|
@ -83,9 +83,9 @@ private extension FollowersCountWidgetProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let desiredAccount = configuration.account ?? authBox.authenticationRecord.object(
|
let desiredAccount = configuration.account ?? authBox.authentication.user(
|
||||||
in: WidgetExtension.appContext.managedObjectContext
|
in: WidgetExtension.appContext.managedObjectContext
|
||||||
)?.user.acctWithDomain
|
)?.acctWithDomain
|
||||||
else {
|
else {
|
||||||
return completion(.unconfigured)
|
return completion(.unconfigured)
|
||||||
}
|
}
|
||||||
|
@ -86,9 +86,9 @@ private extension MultiFollowersCountWidgetProvider {
|
|||||||
|
|
||||||
if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) {
|
if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) {
|
||||||
desiredAccounts = configuredAccounts
|
desiredAccounts = configuredAccounts
|
||||||
} else if let currentlyLoggedInAccount = authBox.authenticationRecord.object(
|
} else if let currentlyLoggedInAccount = authBox.authentication.user(
|
||||||
in: WidgetExtension.appContext.managedObjectContext
|
in: WidgetExtension.appContext.managedObjectContext
|
||||||
)?.user.acctWithDomain {
|
)?.acctWithDomain {
|
||||||
desiredAccounts = [currentlyLoggedInAccount]
|
desiredAccounts = [currentlyLoggedInAccount]
|
||||||
} else {
|
} else {
|
||||||
return completion(.unconfigured)
|
return completion(.unconfigured)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user