forked from shadowfacts/Tusker
Update Mastodon push subscriptions when endpoint changes
This commit is contained in:
parent
840b83012a
commit
68dad77f81
|
@ -36,7 +36,7 @@ class DisabledPushManager: _PushManager {
|
|||
throw Disabled()
|
||||
}
|
||||
|
||||
func updateIfNecessary() async {
|
||||
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
||||
}
|
||||
|
||||
func didRegisterForRemoteNotifications(deviceToken: Data) {
|
||||
|
|
|
@ -51,7 +51,7 @@ public protocol _PushManager {
|
|||
|
||||
func register(transactionID: UInt64) async throws -> PushProxyRegistration
|
||||
func unregister() async throws
|
||||
func updateIfNecessary() async
|
||||
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async
|
||||
|
||||
func didRegisterForRemoteNotifications(deviceToken: Data)
|
||||
func didFailToRegisterForRemoteNotifications(error: any Error)
|
||||
|
|
|
@ -74,7 +74,7 @@ class PushManagerImpl: _PushManager {
|
|||
}
|
||||
let subscription = PushSubscription(
|
||||
accountID: account.id,
|
||||
endpoint: pushProxyRegistration.endpoint,
|
||||
endpoint: endpointURL(registration: pushProxyRegistration, accountID: account.id),
|
||||
secretKey: key,
|
||||
authSecret: authSecret,
|
||||
alerts: [],
|
||||
|
@ -84,6 +84,13 @@ class PushManagerImpl: _PushManager {
|
|||
return subscription
|
||||
}
|
||||
|
||||
private func endpointURL(registration: PushProxyRegistration, accountID: String) -> URL {
|
||||
var endpoint = URLComponents(url: registration.endpoint, resolvingAgainstBaseURL: false)!
|
||||
endpoint.queryItems = endpoint.queryItems ?? []
|
||||
endpoint.queryItems!.append(URLQueryItem(name: "ctx", value: accountID))
|
||||
return endpoint.url!
|
||||
}
|
||||
|
||||
func removeSubscription(account: UserAccountInfo) {
|
||||
pushSubscriptions.removeAll { $0.accountID == account.id }
|
||||
}
|
||||
|
@ -130,7 +137,7 @@ class PushManagerImpl: _PushManager {
|
|||
}
|
||||
}
|
||||
|
||||
func updateIfNecessary() async {
|
||||
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
||||
guard let pushProxyRegistration else {
|
||||
return
|
||||
}
|
||||
|
@ -142,8 +149,19 @@ class PushManagerImpl: _PushManager {
|
|||
return
|
||||
}
|
||||
let newRegistration = try await update(registration: pushProxyRegistration, deviceToken: token)
|
||||
self.pushProxyRegistration = newRegistration
|
||||
if pushProxyRegistration.endpoint != newRegistration.endpoint {
|
||||
// TODO: update subscriptions if the endpoint's changed
|
||||
self.pushSubscriptions = await AsyncSequenceAdaptor(wrapping: self.pushSubscriptions).map {
|
||||
var copy = $0
|
||||
copy.endpoint = await self.endpointURL(registration: newRegistration, accountID: $0.accountID)
|
||||
if await updateSubscription(copy) {
|
||||
return copy
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
}.reduce(into: [], { partialResult, el in
|
||||
partialResult.append(el)
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
PushManager.logger.error("Failed to update push registration: \(String(describing: error), privacy: .public)")
|
||||
|
@ -296,3 +314,25 @@ private extension Data {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AsyncSequenceAdaptor<S: Sequence>: AsyncSequence {
|
||||
typealias Element = S.Element
|
||||
|
||||
let base: S
|
||||
|
||||
init(wrapping base: S) {
|
||||
self.base = base
|
||||
}
|
||||
|
||||
func makeAsyncIterator() -> AsyncIterator {
|
||||
AsyncIterator(base: base.makeIterator())
|
||||
}
|
||||
|
||||
struct AsyncIterator: AsyncIteratorProtocol {
|
||||
var base: S.Iterator
|
||||
|
||||
mutating func next() async -> Element? {
|
||||
base.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ import Foundation
|
|||
import CryptoKit
|
||||
|
||||
public struct PushSubscription {
|
||||
let accountID: String
|
||||
let endpoint: URL
|
||||
public let accountID: String
|
||||
public internal(set) var endpoint: URL
|
||||
public let secretKey: P256.KeyAgreement.PrivateKey
|
||||
public let authSecret: Data
|
||||
public var alerts: Alerts
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||
D630C3C82BC43AFD00208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3C72BC43AFD00208903 /* PushNotifications */; };
|
||||
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3C92BC59FF500208903 /* MastodonController+Push.swift */; };
|
||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||
|
@ -497,6 +498,7 @@
|
|||
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
||||
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonController+Push.swift"; sourceTree = "<group>"; };
|
||||
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
|
||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1651,6 +1653,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */,
|
||||
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */,
|
||||
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */,
|
||||
D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
|
||||
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
||||
|
@ -2228,6 +2231,7 @@
|
|||
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */,
|
||||
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// MastodonController+Push.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/9/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
import PushNotifications
|
||||
import CryptoKit
|
||||
|
||||
extension MastodonController {
|
||||
func createPushSubscription(subscription: PushNotifications.PushSubscription) async throws -> Pachyderm.PushSubscription {
|
||||
let req = Pachyderm.PushSubscription.create(
|
||||
endpoint: subscription.endpoint,
|
||||
// mastodon docs just say "Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve."
|
||||
// other apps use SecKeyCopyExternalRepresentation which is documented to use X9.63 for elliptic curve keys
|
||||
// and that seems to work
|
||||
publicKey: subscription.secretKey.publicKey.x963Representation,
|
||||
authSecret: subscription.authSecret,
|
||||
alerts: .init(subscription.alerts),
|
||||
policy: .init(subscription.policy)
|
||||
)
|
||||
return try await run(req).0
|
||||
}
|
||||
|
||||
func updatePushSubscription(subscription: PushNotifications.PushSubscription) async throws -> Pachyderm.PushSubscription {
|
||||
// when updating anything other than the alerts/policy, we need to go through the create route
|
||||
return try await createPushSubscription(subscription: subscription)
|
||||
}
|
||||
|
||||
func updatePushSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async throws -> Pachyderm.PushSubscription {
|
||||
let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy))
|
||||
return try await run(req).0
|
||||
}
|
||||
|
||||
func deletePushSubscription() async throws {
|
||||
let req = Pachyderm.PushSubscription.delete()
|
||||
_ = try await run(req)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Pachyderm.PushSubscription.Alerts {
|
||||
init(_ alerts: PushNotifications.PushSubscription.Alerts) {
|
||||
self.init(
|
||||
mention: alerts.contains(.mention),
|
||||
status: alerts.contains(.status),
|
||||
reblog: alerts.contains(.reblog),
|
||||
follow: alerts.contains(.follow),
|
||||
followRequest: alerts.contains(.followRequest),
|
||||
favourite: alerts.contains(.favorite),
|
||||
poll: alerts.contains(.poll),
|
||||
update: alerts.contains(.update)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Pachyderm.PushSubscription.Policy {
|
||||
init(_ policy: PushNotifications.PushSubscription.Policy) {
|
||||
switch policy {
|
||||
case .all:
|
||||
self = .all
|
||||
case .followers:
|
||||
self = .followers
|
||||
case .followed:
|
||||
self = .followed
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,10 +84,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
BackgroundManager.shared.registerHandlers()
|
||||
|
||||
Task {
|
||||
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
||||
await PushManager.shared.updateIfNecessary()
|
||||
}
|
||||
initializePushManager()
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -183,6 +180,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
PushManager.shared.didFailToRegisterForRemoteNotifications(error: error)
|
||||
}
|
||||
|
||||
private func initializePushManager() {
|
||||
Task {
|
||||
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
||||
await PushManager.shared.updateIfNecessary(updateSubscription: {
|
||||
guard let account = UserAccountsManager.shared.getAccount(id: $0.accountID) else {
|
||||
return false
|
||||
}
|
||||
let mastodonController = MastodonController.getForAccount(account)
|
||||
do {
|
||||
let result = try await mastodonController.updatePushSubscription(subscription: $0)
|
||||
PushManager.logger.debug("Updated push subscription \(result.id) on \(mastodonController.instanceURL)")
|
||||
return true
|
||||
} catch {
|
||||
PushManager.logger.error("Error updating push subscription: \(String(describing: error))")
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
private func swizzleStatusBar() {
|
||||
let selector = Selector(("handleTapAction:"))
|
||||
|
|
|
@ -68,19 +68,9 @@ struct PushInstanceSettingsView: View {
|
|||
|
||||
private func enableNotifications() async throws {
|
||||
let subscription = try await PushManager.shared.createSubscription(account: account)
|
||||
let req = Pachyderm.PushSubscription.create(
|
||||
endpoint: pushProxyRegistration.endpoint,
|
||||
// mastodon docs just say "Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve."
|
||||
// other apps use SecKeyCopyExternalRepresentation which is documented to use X9.63 for elliptic curve keys
|
||||
// and that seems to work
|
||||
publicKey: subscription.secretKey.publicKey.x963Representation,
|
||||
authSecret: subscription.authSecret,
|
||||
alerts: .init(subscription.alerts),
|
||||
policy: .init(subscription.policy)
|
||||
)
|
||||
let mastodonController = await MastodonController.getForAccount(account)
|
||||
do {
|
||||
let (result, _) = try await mastodonController.run(req)
|
||||
let result = try await mastodonController.createPushSubscription(subscription: subscription)
|
||||
PushManager.logger.debug("Push subscription \(result.id) created on \(account.instanceURL)")
|
||||
self.subscription = subscription
|
||||
} catch {
|
||||
|
@ -91,19 +81,17 @@ struct PushInstanceSettingsView: View {
|
|||
}
|
||||
|
||||
private func disableNotifications() async throws {
|
||||
let req = Pachyderm.PushSubscription.delete()
|
||||
let mastodonController = await MastodonController.getForAccount(account)
|
||||
_ = try await mastodonController.run(req)
|
||||
try await mastodonController.deletePushSubscription()
|
||||
await PushManager.shared.removeSubscription(account: account)
|
||||
subscription = nil
|
||||
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
|
||||
}
|
||||
|
||||
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool {
|
||||
let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy))
|
||||
let mastodonController = await MastodonController.getForAccount(account)
|
||||
do {
|
||||
let (result, _) = try await mastodonController.run(req)
|
||||
let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy)
|
||||
PushManager.logger.debug("Push subscription \(result.id) updated on \(account.instanceURL)")
|
||||
subscription?.alerts = alerts
|
||||
subscription?.policy = policy
|
||||
|
@ -133,34 +121,6 @@ private enum Error: LocalizedError {
|
|||
}
|
||||
}
|
||||
|
||||
private extension Pachyderm.PushSubscription.Alerts {
|
||||
init(_ alerts: PushNotifications.PushSubscription.Alerts) {
|
||||
self.init(
|
||||
mention: alerts.contains(.mention),
|
||||
status: alerts.contains(.status),
|
||||
reblog: alerts.contains(.reblog),
|
||||
follow: alerts.contains(.follow),
|
||||
followRequest: alerts.contains(.followRequest),
|
||||
favourite: alerts.contains(.favorite),
|
||||
poll: alerts.contains(.poll),
|
||||
update: alerts.contains(.update)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Pachyderm.PushSubscription.Policy {
|
||||
init(_ policy: PushNotifications.PushSubscription.Policy) {
|
||||
switch policy {
|
||||
case .all:
|
||||
self = .all
|
||||
case .followers:
|
||||
self = .followers
|
||||
case .followed:
|
||||
self = .followed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// PushInstanceSettingsView()
|
||||
//}
|
||||
|
|
Loading…
Reference in New Issue