IceCubesApp/Packages/Env/Sources/Env/PushNotificationsService.swift
Thomas Ricouard 7f6419ebae Swiftformat
2023-01-17 11:36:01 +01:00

184 lines
6.1 KiB
Swift

import CryptoKit
import Foundation
import KeychainSwift
import Models
import Network
import SwiftUI
import UserNotifications
@MainActor
public class PushNotificationsService: ObservableObject {
enum Constants {
static let endpoint = "https://icecubesrelay.fly.dev"
static let keychainAuthKey = "notifications_auth_key"
static let keychainPrivateKey = "notifications_private_key"
}
public struct PushAccounts {
public let server: String
public let token: OauthToken
public init(server: String, token: OauthToken) {
self.server = server
self.token = token
}
}
public static let shared = PushNotificationsService()
@Published public var pushToken: Data?
@AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true
@Published public var isPushEnabled: Bool = false {
didSet {
if !oldValue && isPushEnabled {
requestPushNotifications()
}
}
}
@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
@Published public var isNewPostsNotificationEnabled: Bool = true
private var subscriptions: [PushSubscription] = []
private var keychain: KeychainSwift {
let keychain = KeychainSwift()
#if !DEBUG
keychain.accessGroup = AppInfo.keychainGroup
#endif
return keychain
}
public func requestPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
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)
} catch {}
}
refreshSubscriptionsUI()
}
public func updateSubscriptions(accounts: [PushAccounts]) async {
subscriptions = []
let key = notificationsPrivateKeyAsKey.publicKey.x963Representation
let authKey = notificationsAuthKeyAsKey
guard let pushToken = pushToken, isUserPushEnabled else { return }
for account in accounts {
let client = Client(server: account.server, oauthToken: account.token)
do {
var listenerURL = Constants.endpoint
listenerURL += "/push/"
listenerURL += pushToken.hexString
listenerURL += "/\(account.server)"
#if DEBUG
listenerURL += "?sandbox=true"
#endif
let sub: PushSubscription =
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
p256dh: key,
auth: authKey,
mentions: isMentionNotificationEnabled,
status: isNewPostsNotificationEnabled,
reblog: isReblogNotificationEnabled,
follow: isFollowNotificationEnabled,
favourite: isFavoriteNotificationEnabled,
poll: isPollNotificationEnabled))
subscriptions.append(sub)
} catch {}
}
refreshSubscriptionsUI()
}
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)
} catch {}
}
await fetchSubscriptions(accounts: accounts)
refreshSubscriptionsUI()
}
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
isNewPostsNotificationEnabled = sub.alerts.status
} else {
isPushEnabled = false
}
}
// MARK: - Key management
public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey {
if let key = keychain.get(Constants.keychainPrivateKey),
let data = Data(base64Encoded: key)
{
do {
return try P256.KeyAgreement.PrivateKey(rawRepresentation: data)
} catch {
let key = P256.KeyAgreement.PrivateKey()
keychain.set(key.rawRepresentation.base64EncodedString(),
forKey: Constants.keychainPrivateKey,
withAccess: .accessibleAfterFirstUnlock)
return key
}
} else {
let key = P256.KeyAgreement.PrivateKey()
keychain.set(key.rawRepresentation.base64EncodedString(),
forKey: Constants.keychainPrivateKey,
withAccess: .accessibleAfterFirstUnlock)
return key
}
}
public var notificationsAuthKeyAsKey: Data {
if let key = keychain.get(Constants.keychainAuthKey),
let data = Data(base64Encoded: key)
{
return data
} else {
let key = Self.makeRandomeNotificationsAuthKey()
keychain.set(key.base64EncodedString(),
forKey: Constants.keychainAuthKey,
withAccess: .accessibleAfterFirstUnlock)
return key
}
}
private static func makeRandomeNotificationsAuthKey() -> Data {
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()
}
}