mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 00:11:00 +00:00
Cleanup + Polish push notifications
This commit is contained in:
parent
a68efaef56
commit
ed8208857a
10 changed files with 127 additions and 103 deletions
|
@ -19,6 +19,7 @@
|
|||
9F2A5424296AB67A009B2D7C /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A5423296AB67A009B2D7C /* Env */; };
|
||||
9F2A5426296AB67E009B2D7C /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A5425296AB67E009B2D7C /* KeychainSwift */; };
|
||||
9F2A5428296AB683009B2D7C /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A5427296AB683009B2D7C /* Models */; };
|
||||
9F2A542A296AF557009B2D7C /* NotificationServiceSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5429296AF557009B2D7C /* NotificationServiceSupport.swift */; };
|
||||
9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; };
|
||||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; };
|
||||
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */; };
|
||||
|
@ -88,6 +89,7 @@
|
|||
9F2A5418296AB631009B2D7C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
9F2A541A296AB631009B2D7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9F2A5422296AB64B009B2D7C /* IceCubesNotifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesNotifications.entitlements; sourceTree = "<group>"; };
|
||||
9F2A5429296AF557009B2D7C /* NotificationServiceSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceSupport.swift; sourceTree = "<group>"; };
|
||||
9F2B92F5295AE04800DE16D0 /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = "<group>"; };
|
||||
9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = "<group>"; };
|
||||
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = "<group>"; };
|
||||
|
@ -165,6 +167,7 @@
|
|||
children = (
|
||||
9F2A5422296AB64B009B2D7C /* IceCubesNotifications.entitlements */,
|
||||
9F2A5418296AB631009B2D7C /* NotificationService.swift */,
|
||||
9F2A5429296AF557009B2D7C /* NotificationServiceSupport.swift */,
|
||||
9F2A541A296AB631009B2D7C /* Info.plist */,
|
||||
);
|
||||
path = IceCubesNotifications;
|
||||
|
@ -414,6 +417,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F2A542A296AF557009B2D7C /* NotificationServiceSupport.swift in Sources */,
|
||||
9F2A5419296AB631009B2D7C /* NotificationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -15,7 +15,7 @@ class AppAccountsManager: ObservableObject {
|
|||
@Published var availableAccounts: [AppAccount]
|
||||
@Published var currentClient: Client
|
||||
|
||||
var pushAccounts: [PushNotifications.PushAccounts] {
|
||||
var pushAccounts: [PushNotificationsService.PushAccounts] {
|
||||
availableAccounts.filter{ $0.oauthToken != nil}
|
||||
.map{ .init(server: $0.server, token: $0.oauthToken!) }
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ struct IceCubesApp: App {
|
|||
.environmentObject(userPreferences)
|
||||
.environmentObject(theme)
|
||||
.environmentObject(watcher)
|
||||
.environmentObject(PushNotifications.shared)
|
||||
.environmentObject(PushNotificationsService.shared)
|
||||
.quickLookPreview($quickLook.url, in: quickLook.urls)
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { scenePhase in
|
||||
|
@ -154,7 +154,7 @@ struct IceCubesApp: App {
|
|||
}
|
||||
|
||||
private func refreshPushSubs() {
|
||||
PushNotifications.shared.requestPushNotifications()
|
||||
PushNotificationsService.shared.requestPushNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,10 +166,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
|
||||
func application(_ application: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
PushNotifications.shared.pushToken = deviceToken
|
||||
PushNotificationsService.shared.pushToken = deviceToken
|
||||
Task {
|
||||
await PushNotifications.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotifications.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ struct AddAccountView: View {
|
|||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var pushNotifications: PushNotifications
|
||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var instanceName: String = ""
|
||||
|
|
|
@ -9,7 +9,7 @@ import Env
|
|||
struct PushNotificationsView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var pushNotifications: PushNotifications
|
||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||
|
||||
@State private var subscriptions: [PushSubscription] = []
|
||||
|
||||
|
@ -19,25 +19,30 @@ struct PushNotificationsView: View {
|
|||
Toggle(isOn: $pushNotifications.isPushEnabled) {
|
||||
Text("Push notification")
|
||||
}
|
||||
} footer: {
|
||||
Text("Receive push notifications on new activities")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
if pushNotifications.isPushEnabled {
|
||||
Section {
|
||||
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
|
||||
Label("Mentions", systemImage: "at")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isFollowNotificationEnabled) {
|
||||
Text("Follow notification")
|
||||
Label("Follows", systemImage: "person.badge.plus")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isFavoriteNotificationEnabled) {
|
||||
Text("Favorite notification")
|
||||
Label("Favorites", systemImage: "star")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isReblogNotificationEnabled) {
|
||||
Text("Boost notification")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
|
||||
Text("Mention notification")
|
||||
Label("Boosts", systemImage: "arrow.left.arrow.right.circle")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isPollNotificationEnabled) {
|
||||
Text("Polls notification")
|
||||
Label("Polls Results", systemImage: "chart.bar")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isNewPostsNotificationEnabled) {
|
||||
Label("New Posts", systemImage: "bubble.right")
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
@ -77,6 +82,9 @@ struct PushNotificationsView: View {
|
|||
.onChange(of: pushNotifications.isFavoriteNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isNewPostsNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSubscriptions() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import Models
|
|||
import DesignSystem
|
||||
|
||||
struct SettingsTabs: View {
|
||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var client: Client
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
|
@ -57,6 +58,11 @@ struct SettingsTabs: View {
|
|||
.onDelete { indexSet in
|
||||
if let index = indexSet.first {
|
||||
let account = appAccountsManager.availableAccounts[index]
|
||||
if let token = account.oauthToken {
|
||||
Task {
|
||||
await pushNotifications.deleteSubscriptions(accounts: [.init(server: account.server, token: token)])
|
||||
}
|
||||
}
|
||||
appAccountsManager.delete(account: account)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,10 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
bestAttemptContent?.title = "A new notification have been received"
|
||||
|
||||
if let bestAttemptContent {
|
||||
let privateKey = PushNotifications.shared.notificationsPrivateKeyAsKey
|
||||
let auth = PushNotifications.shared.notificationsAuthKeyAsKey
|
||||
let privateKey = PushNotificationsService.shared.notificationsPrivateKeyAsKey
|
||||
let auth = PushNotificationsService.shared.notificationsAuthKeyAsKey
|
||||
|
||||
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
|
||||
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else {
|
||||
|
@ -80,86 +79,4 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? {
|
||||
guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32)
|
||||
|
||||
let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16)
|
||||
|
||||
let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
|
||||
|
||||
let nonceData = nonce.withUnsafeBytes(Array.init)
|
||||
|
||||
guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _plaintext: Data?
|
||||
do {
|
||||
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
guard let plaintext = _plaintext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
||||
guard plaintext.count >= 2 + paddingLength else {
|
||||
print("1")
|
||||
fatalError()
|
||||
}
|
||||
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
||||
|
||||
return Data(unpadded)
|
||||
}
|
||||
|
||||
static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data {
|
||||
var info = Data()
|
||||
|
||||
info.append("Content-Encoding: ".data(using: .utf8)!)
|
||||
info.append(type.data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append("P-256".data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(clientPublicKey)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(serverPublicKey)
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func escape() -> String {
|
||||
return self
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: """, with: "\"")
|
||||
.replacingOccurrences(of: "'", with: "'")
|
||||
.replacingOccurrences(of: "'", with: "’")
|
||||
|
||||
}
|
||||
|
||||
func URLSafeBase64ToBase64() -> String {
|
||||
var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||
let countMod4 = count % 4
|
||||
|
||||
if countMod4 != 0 {
|
||||
base64.append(String(repeating: "=", count: 4 - countMod4))
|
||||
}
|
||||
|
||||
return base64
|
||||
}
|
||||
}
|
||||
|
||||
|
|
86
IceCubesNotifications/NotificationServiceSupport.swift
Normal file
86
IceCubesNotifications/NotificationServiceSupport.swift
Normal file
|
@ -0,0 +1,86 @@
|
|||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
extension NotificationService {
|
||||
static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? {
|
||||
guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32)
|
||||
|
||||
let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16)
|
||||
|
||||
let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
|
||||
|
||||
let nonceData = nonce.withUnsafeBytes(Array.init)
|
||||
|
||||
guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _plaintext: Data?
|
||||
do {
|
||||
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
guard let plaintext = _plaintext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
||||
guard plaintext.count >= 2 + paddingLength else {
|
||||
print("1")
|
||||
fatalError()
|
||||
}
|
||||
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
||||
|
||||
return Data(unpadded)
|
||||
}
|
||||
|
||||
static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data {
|
||||
var info = Data()
|
||||
|
||||
info.append("Content-Encoding: ".data(using: .utf8)!)
|
||||
info.append(type.data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append("P-256".data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(clientPublicKey)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(serverPublicKey)
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func escape() -> String {
|
||||
return self
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: """, with: "\"")
|
||||
.replacingOccurrences(of: "'", with: "'")
|
||||
.replacingOccurrences(of: "'", with: "’")
|
||||
|
||||
}
|
||||
|
||||
func URLSafeBase64ToBase64() -> String {
|
||||
var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||
let countMod4 = count % 4
|
||||
|
||||
if countMod4 != 0 {
|
||||
base64.append(String(repeating: "=", count: 4 - countMod4))
|
||||
}
|
||||
|
||||
return base64
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import Models
|
|||
import Network
|
||||
|
||||
@MainActor
|
||||
public class PushNotifications: ObservableObject {
|
||||
public class PushNotificationsService: ObservableObject {
|
||||
enum Constants {
|
||||
static let endpoint = "https://icecubesrelay.fly.dev"
|
||||
static let keychainGroup = "346J38YKE3.com.thomasricouard.IceCubesApp"
|
||||
|
@ -25,7 +25,7 @@ public class PushNotifications: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public static let shared = PushNotifications()
|
||||
public static let shared = PushNotificationsService()
|
||||
|
||||
@Published public var pushToken: Data?
|
||||
|
||||
|
@ -42,6 +42,7 @@ public class PushNotifications: ObservableObject {
|
|||
@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] = []
|
||||
|
||||
|
@ -89,7 +90,7 @@ public class PushNotifications: ObservableObject {
|
|||
p256dh: notificationsPrivateKeyAsKey.publicKey.x963Representation,
|
||||
auth: notificationsAuthKeyAsKey,
|
||||
mentions: isMentionNotificationEnabled,
|
||||
status: true,
|
||||
status: isNewPostsNotificationEnabled,
|
||||
reblog: isReblogNotificationEnabled,
|
||||
follow: isFollowNotificationEnabled,
|
||||
favourite: isFavoriteNotificationEnabled,
|
||||
|
@ -119,6 +120,7 @@ public class PushNotifications: ObservableObject {
|
|||
isReblogNotificationEnabled = sub.alerts.reblog
|
||||
isMentionNotificationEnabled = sub.alerts.mention
|
||||
isPollNotificationEnabled = sub.alerts.poll
|
||||
isNewPostsNotificationEnabled = sub.alerts.status
|
||||
} else {
|
||||
isPushEnabled = false
|
||||
}
|
|
@ -7,6 +7,7 @@ public struct PushSubscription: Identifiable, Decodable {
|
|||
public let reblog: Bool
|
||||
public let mention: Bool
|
||||
public let poll: Bool
|
||||
public let status: Bool
|
||||
}
|
||||
|
||||
public let id: Int
|
||||
|
|
Loading…
Reference in a new issue