2023-01-17 10:36:01 +00:00
|
|
|
import CryptoKit
|
2023-01-08 09:22:52 +00:00
|
|
|
import Foundation
|
|
|
|
import KeychainSwift
|
|
|
|
import Models
|
|
|
|
import Network
|
2023-01-17 10:36:01 +00:00
|
|
|
import SwiftUI
|
|
|
|
import UserNotifications
|
2023-01-08 09:22:52 +00:00
|
|
|
|
|
|
|
@MainActor
|
2023-01-08 13:16:43 +00:00
|
|
|
public class PushNotificationsService: ObservableObject {
|
2023-01-08 09:22:52 +00:00
|
|
|
enum Constants {
|
|
|
|
static let endpoint = "https://icecubesrelay.fly.dev"
|
|
|
|
static let keychainAuthKey = "notifications_auth_key"
|
|
|
|
static let keychainPrivateKey = "notifications_private_key"
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public struct PushAccounts {
|
|
|
|
public let server: String
|
|
|
|
public let token: OauthToken
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public init(server: String, token: OauthToken) {
|
|
|
|
self.server = server
|
|
|
|
self.token = token
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 13:16:43 +00:00
|
|
|
public static let shared = PushNotificationsService()
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
@Published public var pushToken: Data?
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
@AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true
|
|
|
|
@Published public var isPushEnabled: Bool = false {
|
|
|
|
didSet {
|
|
|
|
if !oldValue && isPushEnabled {
|
|
|
|
requestPushNotifications()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
@Published public var isFollowNotificationEnabled: Bool = true
|
|
|
|
@Published public var isFavoriteNotificationEnabled: Bool = true
|
|
|
|
@Published public var isReblogNotificationEnabled: Bool = true
|
|
|
|
@Published public var isMentionNotificationEnabled: Bool = true
|
|
|
|
@Published public var isPollNotificationEnabled: Bool = true
|
2023-01-08 13:16:43 +00:00
|
|
|
@Published public var isNewPostsNotificationEnabled: Bool = true
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
private var subscriptions: [PushSubscription] = []
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
private var keychain: KeychainSwift {
|
|
|
|
let keychain = KeychainSwift()
|
2023-01-09 17:52:53 +00:00
|
|
|
#if !DEBUG
|
2023-01-12 17:17:21 +00:00
|
|
|
keychain.accessGroup = AppInfo.keychainGroup
|
2023-01-09 17:52:53 +00:00
|
|
|
#endif
|
2023-01-08 09:22:52 +00:00
|
|
|
return keychain
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public func requestPushNotifications() {
|
2023-01-17 10:36:01 +00:00
|
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in
|
2023-01-08 09:22:52 +00:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public func fetchSubscriptions(accounts: [PushAccounts]) async {
|
|
|
|
subscriptions = []
|
|
|
|
for account in accounts {
|
|
|
|
let client = Client(server: account.server, oauthToken: account.token)
|
|
|
|
do {
|
|
|
|
let sub: PushSubscription = try await client.get(endpoint: Push.subscription)
|
|
|
|
subscriptions.append(sub)
|
2023-01-17 10:36:01 +00:00
|
|
|
} catch {}
|
2023-01-08 09:22:52 +00:00
|
|
|
}
|
|
|
|
refreshSubscriptionsUI()
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public func updateSubscriptions(accounts: [PushAccounts]) async {
|
|
|
|
subscriptions = []
|
2023-01-08 15:18:38 +00:00
|
|
|
let key = notificationsPrivateKeyAsKey.publicKey.x963Representation
|
|
|
|
let authKey = notificationsAuthKeyAsKey
|
2023-01-08 09:22:52 +00:00
|
|
|
guard let pushToken = pushToken, isUserPushEnabled else { return }
|
|
|
|
for account in accounts {
|
|
|
|
let client = Client(server: account.server, oauthToken: account.token)
|
2023-01-17 10:36:01 +00:00
|
|
|
do {
|
|
|
|
var listenerURL = Constants.endpoint
|
|
|
|
listenerURL += "/push/"
|
|
|
|
listenerURL += pushToken.hexString
|
|
|
|
listenerURL += "/\(account.server)"
|
|
|
|
#if DEBUG
|
|
|
|
listenerURL += "?sandbox=true"
|
|
|
|
#endif
|
|
|
|
let sub: PushSubscription =
|
2023-01-08 09:22:52 +00:00
|
|
|
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
|
2023-01-08 15:18:38 +00:00
|
|
|
p256dh: key,
|
|
|
|
auth: authKey,
|
2023-01-08 09:22:52 +00:00
|
|
|
mentions: isMentionNotificationEnabled,
|
2023-01-08 13:16:43 +00:00
|
|
|
status: isNewPostsNotificationEnabled,
|
2023-01-08 09:22:52 +00:00
|
|
|
reblog: isReblogNotificationEnabled,
|
|
|
|
follow: isFollowNotificationEnabled,
|
|
|
|
favourite: isFavoriteNotificationEnabled,
|
|
|
|
poll: isPollNotificationEnabled))
|
2023-01-17 10:36:01 +00:00
|
|
|
subscriptions.append(sub)
|
|
|
|
} catch {}
|
|
|
|
}
|
2023-01-08 09:22:52 +00:00
|
|
|
refreshSubscriptionsUI()
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public func deleteSubscriptions(accounts: [PushAccounts]) async {
|
|
|
|
for account in accounts {
|
|
|
|
let client = Client(server: account.server, oauthToken: account.token)
|
|
|
|
do {
|
|
|
|
_ = try await client.delete(endpoint: Push.subscription)
|
2023-01-17 10:36:01 +00:00
|
|
|
} catch {}
|
2023-01-08 09:22:52 +00:00
|
|
|
}
|
|
|
|
await fetchSubscriptions(accounts: accounts)
|
|
|
|
refreshSubscriptionsUI()
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
private func refreshSubscriptionsUI() {
|
|
|
|
if let sub = subscriptions.first {
|
|
|
|
isPushEnabled = true
|
|
|
|
isFollowNotificationEnabled = sub.alerts.follow
|
|
|
|
isFavoriteNotificationEnabled = sub.alerts.favourite
|
|
|
|
isReblogNotificationEnabled = sub.alerts.reblog
|
|
|
|
isMentionNotificationEnabled = sub.alerts.mention
|
|
|
|
isPollNotificationEnabled = sub.alerts.poll
|
2023-01-08 13:16:43 +00:00
|
|
|
isNewPostsNotificationEnabled = sub.alerts.status
|
2023-01-08 09:22:52 +00:00
|
|
|
} else {
|
|
|
|
isPushEnabled = false
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
// MARK: - Key management
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey {
|
|
|
|
if let key = keychain.get(Constants.keychainPrivateKey),
|
2023-01-17 10:36:01 +00:00
|
|
|
let data = Data(base64Encoded: key)
|
|
|
|
{
|
2023-01-08 09:22:52 +00:00
|
|
|
do {
|
|
|
|
return try P256.KeyAgreement.PrivateKey(rawRepresentation: data)
|
|
|
|
} catch {
|
|
|
|
let key = P256.KeyAgreement.PrivateKey()
|
2023-01-08 10:22:44 +00:00
|
|
|
keychain.set(key.rawRepresentation.base64EncodedString(),
|
|
|
|
forKey: Constants.keychainPrivateKey,
|
|
|
|
withAccess: .accessibleAfterFirstUnlock)
|
2023-01-08 09:22:52 +00:00
|
|
|
return key
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let key = P256.KeyAgreement.PrivateKey()
|
2023-01-08 10:22:44 +00:00
|
|
|
keychain.set(key.rawRepresentation.base64EncodedString(),
|
|
|
|
forKey: Constants.keychainPrivateKey,
|
|
|
|
withAccess: .accessibleAfterFirstUnlock)
|
2023-01-08 09:22:52 +00:00
|
|
|
return key
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-08 09:22:52 +00:00
|
|
|
public var notificationsAuthKeyAsKey: Data {
|
|
|
|
if let key = keychain.get(Constants.keychainAuthKey),
|
2023-01-17 10:36:01 +00:00
|
|
|
let data = Data(base64Encoded: key)
|
|
|
|
{
|
2023-01-08 09:22:52 +00:00
|
|
|
return data
|
|
|
|
} else {
|
|
|
|
let key = Self.makeRandomeNotificationsAuthKey()
|
2023-01-08 10:22:44 +00:00
|
|
|
keychain.set(key.base64EncodedString(),
|
|
|
|
forKey: Constants.keychainAuthKey,
|
|
|
|
withAccess: .accessibleAfterFirstUnlock)
|
2023-01-08 09:22:52 +00:00
|
|
|
return key
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
|
|
|
private static func makeRandomeNotificationsAuthKey() -> Data {
|
2023-01-08 09:22:52 +00:00
|
|
|
let byteCount = 16
|
|
|
|
var bytes = Data(count: byteCount)
|
|
|
|
_ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) }
|
|
|
|
return bytes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Data {
|
|
|
|
var hexString: String {
|
|
|
|
return map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
|
|
|
|
}
|
|
|
|
}
|