Refactoring

This commit is contained in:
Justin Mazzocchi 2020-08-13 18:24:53 -07:00
parent 39a7b24370
commit 43ccc12468
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
10 changed files with 93 additions and 118 deletions

View File

@ -38,7 +38,8 @@ extension IdentityDatabase {
lastUsedAt: Date(),
preferences: Identity.Preferences(),
instanceURI: nil,
pushSubscriptionAlerts: nil)
lastRegisteredDeviceToken: nil,
pushSubscriptionAlerts: .initial)
.save)
.eraseToAnyPublisher()
}
@ -202,7 +203,7 @@ private extension IdentityDatabase {
.indexed()
.references("instance", column: "uri")
t.column("preferences", .blob).notNull()
t.column("pushSubscriptionAlerts", .blob)
t.column("pushSubscriptionAlerts", .blob).notNull()
t.column("lastRegisteredDeviceToken", .text)
}
@ -233,7 +234,8 @@ private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord,
let lastUsedAt: Date
let preferences: Identity.Preferences
let instanceURI: String?
let pushSubscriptionAlerts: PushSubscription.Alerts?
let lastRegisteredDeviceToken: String?
let pushSubscriptionAlerts: PushSubscription.Alerts
}
extension StoredIdentity {
@ -253,7 +255,7 @@ private struct IdentityResult: Codable, Hashable, FetchableRecord {
let identity: StoredIdentity
let instance: Identity.Instance?
let account: Identity.Account?
let pushSubscriptionAlerts: PushSubscription.Alerts?
let pushSubscriptionAlerts: PushSubscription.Alerts
}
private extension Identity {
@ -265,6 +267,7 @@ private extension Identity {
preferences: result.identity.preferences,
instance: result.instance,
account: result.account,
lastRegisteredDeviceToken: result.identity.lastRegisteredDeviceToken,
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
}
}

View File

@ -9,7 +9,8 @@ struct Identity: Codable, Hashable, Identifiable {
let preferences: Identity.Preferences
let instance: Identity.Instance?
let account: Identity.Account?
let pushSubscriptionAlerts: PushSubscription.Alerts?
let lastRegisteredDeviceToken: String?
let pushSubscriptionAlerts: PushSubscription.Alerts
}
extension Identity {

View File

@ -15,3 +15,7 @@ struct PushSubscription: Codable {
let alerts: Alerts
let serverKey: String
}
extension PushSubscription.Alerts {
static let initial: Self = Self(follow: true, favourite: true, reblog: true, mention: true, poll: true)
}

View File

@ -7,13 +7,9 @@ enum PushSubscriptionEndpoint {
endpoint: URL,
publicKey: String,
auth: String,
follow: Bool,
favourite: Bool,
reblog: Bool,
mention: Bool,
poll: Bool)
alerts: PushSubscription.Alerts)
case read
case update(follow: Bool, favourite: Bool, reblog: Bool, mention: Bool, poll: Bool)
case update(alerts: PushSubscription.Alerts)
case delete
}
@ -37,7 +33,7 @@ extension PushSubscriptionEndpoint: MastodonEndpoint {
var parameters: [String: Any]? {
switch self {
case let .create(endpoint, publicKey, auth, follow, favourite, reblog, mention, poll):
case let .create(endpoint, publicKey, auth, alerts):
return ["subscription":
["endpoint": endpoint.absoluteString,
"keys": [
@ -45,20 +41,20 @@ extension PushSubscriptionEndpoint: MastodonEndpoint {
"auth": auth]],
"data": [
"alerts": [
"follow": follow,
"favourite": favourite,
"reblog": reblog,
"mention": mention,
"poll": poll
"follow": alerts.follow,
"favourite": alerts.favourite,
"reblog": alerts.reblog,
"mention": alerts.mention,
"poll": alerts.poll
]]]
case let .update(follow, favourite, reblog, mention, poll):
case let .update(alerts):
return ["data":
["alerts":
["follow": follow,
"favourite": favourite,
"reblog": reblog,
"mention": mention,
"poll": poll]]]
["follow": alerts.follow,
"favourite": alerts.favourite,
"reblog": alerts.reblog,
"mention": alerts.mention,
"poll": alerts.poll]]]
default: return nil
}
}

View File

@ -65,82 +65,20 @@ extension IdentitiesService {
.eraseToAnyPublisher()
}
func updatePushSubscription(
identityID: UUID,
instanceURL: URL,
deviceToken: String,
alerts: PushSubscription.Alerts?) -> AnyPublisher<Void, Error> {
let secretsService = SecretsService(
identityID: identityID,
keychainServiceType: environment.keychainServiceType)
let accessTokenOptional: String?
do {
accessTokenOptional = try secretsService.item(.accessToken) as String?
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
guard let accessToken: String = accessTokenOptional
else { return Empty().eraseToAnyPublisher() }
let publicKey: String
let auth: String
do {
publicKey = try secretsService.generatePushKeyAndReturnPublicKey().base64EncodedString()
auth = try secretsService.generatePushAuth().base64EncodedString()
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
let networkClient = MastodonClient(session: environment.session)
networkClient.instanceURL = instanceURL
networkClient.accessToken = accessToken
let endpoint = Self.pushSubscriptionEndpointURL
.appendingPathComponent(deviceToken)
.appendingPathComponent(identityID.uuidString)
return networkClient.request(
PushSubscriptionEndpoint.create(
endpoint: endpoint,
publicKey: publicKey,
auth: auth,
follow: alerts?.follow ?? true,
favourite: alerts?.favourite ?? true,
reblog: alerts?.reblog ?? true,
mention: alerts?.mention ?? true,
poll: alerts?.poll ?? true))
.map { (deviceToken, $0.alerts, identityID) }
.flatMap(identityDatabase.updatePushSubscription(deviceToken:alerts:forIdentityID:))
.eraseToAnyPublisher()
}
func updatePushSubscriptions(deviceToken: String) -> AnyPublisher<Void, Error> {
identityDatabase.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken)
.flatMap { identities -> Publishers.MergeMany<AnyPublisher<Void, Never>> in
Publishers.MergeMany(
identities.map { [weak self] in
guard let self = self else { return Empty().eraseToAnyPublisher() }
.tryMap { [weak self] identities -> [AnyPublisher<Void, Never>] in
guard let self = self else { return [Empty().eraseToAnyPublisher()] }
return self.updatePushSubscription(
identityID: $0.id,
instanceURL: $0.url,
deviceToken: deviceToken,
alerts: $0.pushSubscriptionAlerts)
.catch { _ in Empty() } // can't let one failure stop the pipeline
.eraseToAnyPublisher()
})
return try identities.map {
try self.identityService(id: $0.id)
.createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts)
.catch { _ in Empty() } // don't want to disrupt pipeline, consider future telemetry
.eraseToAnyPublisher()
}
}
.map(Publishers.MergeMany.init)
.map { _ in () }
.eraseToAnyPublisher()
}
}
private extension IdentitiesService {
#if DEBUG
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push?sandbox=true")!
#else
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push")!
#endif
}

View File

@ -10,6 +10,7 @@ class IdentityService {
private let identityDatabase: IdentityDatabase
private let environment: AppEnvironment
private let networkClient: MastodonClient
private let secretsService: SecretsService
private let observationErrorsInput = PassthroughSubject<Error, Never>()
init(identityID: UUID,
@ -29,12 +30,12 @@ class IdentityService {
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity
networkClient = MastodonClient(session: environment.session)
networkClient.instanceURL = identity.url
networkClient.accessToken = try SecretsService(
secretsService = SecretsService(
identityID: identityID,
keychainServiceType: environment.keychainServiceType)
.item(.accessToken)
networkClient = MastodonClient(session: environment.session)
networkClient.instanceURL = identity.url
networkClient.accessToken = try secretsService.item(.accessToken)
observation.catch { [weak self] error -> Empty<Identity, Never> in
self?.observationErrorsInput.send(error)
@ -85,4 +86,39 @@ extension IdentityService {
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Void, Error> {
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
}
func createPushSubscription(deviceToken: String, alerts: PushSubscription.Alerts) -> AnyPublisher<Void, Error> {
let publicKey: String
let auth: String
do {
publicKey = try secretsService.generatePushKeyAndReturnPublicKey().base64EncodedString()
auth = try secretsService.generatePushAuth().base64EncodedString()
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
let identityID = identity.id
let endpoint = Self.pushSubscriptionEndpointURL
.appendingPathComponent(deviceToken)
.appendingPathComponent(identityID.uuidString)
return networkClient.request(
PushSubscriptionEndpoint.create(
endpoint: endpoint,
publicKey: publicKey,
auth: auth,
alerts: alerts))
.map { (deviceToken, $0.alerts, identityID) }
.flatMap(identityDatabase.updatePushSubscription(deviceToken:alerts:forIdentityID:))
.eraseToAnyPublisher()
}
}
private extension IdentityService {
#if DEBUG
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push?sandbox=true")!
#else
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push")!
#endif
}

View File

@ -7,15 +7,15 @@ class AddIdentityViewModel: ObservableObject {
@Published var urlFieldText = ""
@Published var alertItem: AlertItem?
@Published private(set) var loading = false
let addedIdentityIDAndURL: AnyPublisher<(UUID, URL), Never>
let addedIdentityID: AnyPublisher<UUID, Never>
private let identitiesService: IdentitiesService
private let addedIdentityIDAndURLInput = PassthroughSubject<(UUID, URL), Never>()
private let addedIdentityIDInput = PassthroughSubject<UUID, Never>()
private var cancellables = Set<AnyCancellable>()
init(identitiesService: IdentitiesService) {
self.identitiesService = identitiesService
addedIdentityIDAndURL = addedIdentityIDAndURLInput.eraseToAnyPublisher()
addedIdentityID = addedIdentityIDInput.eraseToAnyPublisher()
}
func logInTapped() {
@ -33,13 +33,13 @@ class AddIdentityViewModel: ObservableObject {
identitiesService.authorizeIdentity(id: identityID, instanceURL: instanceURL)
.map { (identityID, instanceURL) }
.flatMap(identitiesService.createIdentity(id:instanceURL:))
.map { (identityID, instanceURL) }
.map { identityID }
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.receive(on: RunLoop.main)
.handleEvents(
receiveSubscription: { [weak self] _ in self?.loading = true },
receiveCompletion: { [weak self] _ in self?.loading = false })
.sink(receiveValue: addedIdentityIDAndURLInput.send)
.sink(receiveValue: addedIdentityIDInput.send)
.store(in: &cancellables)
}
@ -57,9 +57,9 @@ class AddIdentityViewModel: ObservableObject {
// TODO: Ensure instance has not disabled public preview
identitiesService.createIdentity(id: identityID, instanceURL: instanceURL)
.map { (identityID, instanceURL) }
.map { identityID }
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink(receiveValue: addedIdentityIDAndURLInput.send)
.sink(receiveValue: addedIdentityIDInput.send)
.store(in: &cancellables)
}
}

View File

@ -55,22 +55,19 @@ extension RootViewModel {
.store(in: &cancellables)
identityService.updateLastUse()
.sink(receiveCompletion: { _ in }, receiveValue: {})
.sink { _ in } receiveValue: { _ in }
.store(in: &cancellables)
mainNavigationViewModel = MainNavigationViewModel(identityService: identityService)
}
func newIdentityCreated(id: UUID, instanceURL: URL) {
newIdentitySelected(id: id)
userNotificationService.isAuthorized()
.filter { $0 }
.zip(appDelegate.registerForRemoteNotifications())
.map { (id, instanceURL, $1, nil) }
.flatMap(identitiesService.updatePushSubscription(identityID:instanceURL:deviceToken:alerts:))
.filter { identityService.identity.lastRegisteredDeviceToken != $1 }
.map { ($1, identityService.identity.pushSubscriptionAlerts) }
.flatMap(identityService.createPushSubscription(deviceToken:alerts:))
.sink { _ in } receiveValue: { _ in }
.store(in: &cancellables)
mainNavigationViewModel = MainNavigationViewModel(identityService: identityService)
}
func deleteIdentity(id: UUID) {

View File

@ -34,9 +34,9 @@ struct AddIdentityView: View {
}
.paddingIfMac()
.alertItem($viewModel.alertItem)
.onReceive(viewModel.addedIdentityIDAndURL) { id, url in
.onReceive(viewModel.addedIdentityID) { id in
withAnimation {
rootViewModel.newIdentityCreated(id: id, instanceURL: url)
rootViewModel.newIdentitySelected(id: id)
}
}
}

View File

@ -13,7 +13,7 @@ class RootViewModelTests: XCTestCase {
identitiesService: IdentitiesService(
identityDatabase: .fresh(),
environment: .development),
notificationService: NotificationService())
userNotificationService: UserNotificationService())
let recorder = sut.$mainNavigationViewModel.record()
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))