From 4c078e8e279d7f4e46c1fc8c4c6f94383f62de78 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 8 Jul 2026 13:52:58 +0200 Subject: [PATCH 1/3] WIP Signed-off-by: Milen Pivchev --- .../NotificationService.swift | 125 ++++++++++++------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index fefe26ca70..af769aafdb 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -21,12 +21,14 @@ import UIKit import UserNotifications +import os import NextcloudKit class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? var request: UNNotificationRequest? + private let deliveryLock = OSAllocatedUnfairLock() override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler @@ -35,59 +37,100 @@ class NotificationService: UNNotificationServiceExtension { NextcloudKit.configureLogger(logLevel: .verbose) - if let bestAttemptContent = bestAttemptContent { - bestAttemptContent.title = "" - bestAttemptContent.body = "Nextcloud notification" - do { - if let message = bestAttemptContent.userInfo["subject"] as? String { - for tableAccount in NCManageDatabase.shared.getAllTableAccount() { - guard let privateKey = NCPreferences().getPushNotificationPrivateKey(account: tableAccount.account) else { - bestAttemptContent.body = "Error retrieving private key for \(tableAccount.account)" - continue - } + guard let bestAttemptContent else { return } - let prefixData = Data(privateKey.prefix(8)) - let prefixBase64 = prefixData.base64EncodedString() - nkLog(debug: "🔑 Loaded private key for \(tableAccount.account): prefix(Base64)=\(prefixBase64)") + bestAttemptContent.title = "" + bestAttemptContent.body = "Nextcloud notification" - guard let decryptedMessage = NCPushNotificationEncryption.shared().decryptPushNotification(message, withDevicePrivateKey: privateKey) else { - bestAttemptContent.body = "Error decryption for \(tableAccount.account)" - continue - } - guard let data = decryptedMessage.data(using: .utf8) else { - bestAttemptContent.body = "Error decryption data utf8 for \(tableAccount.account)" - continue - } + var matchedAccount: tableAccount? + var payload: [String: AnyObject]? + + do { + if let message = bestAttemptContent.userInfo["subject"] as? String { + for tableAccount in NCManageDatabase.shared.getAllTableAccount() { + guard let privateKey = NCPreferences().getPushNotificationPrivateKey(account: tableAccount.account) else { + bestAttemptContent.body = "Error retrieving private key for \(tableAccount.account)" + continue + } + + let prefixData = Data(privateKey.prefix(8)) + let prefixBase64 = prefixData.base64EncodedString() + nkLog(debug: "🔑 Loaded private key for \(tableAccount.account): prefix(Base64)=\(prefixBase64)") - if var json = try JSONSerialization.jsonObject(with: data) as? [String: AnyObject], - let subject = json["subject"] as? String { - bestAttemptContent.body = subject - if let pref = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) { - json["account"] = tableAccount.account as AnyObject - pref.set(json, forKey: "NOTIFICATION_DATA") - pref.synchronize() - } - } else { - bestAttemptContent.body = "Error JSON Serialization for \(tableAccount.account)" + guard let decryptedMessage = NCPushNotificationEncryption.shared().decryptPushNotification(message, withDevicePrivateKey: privateKey) else { + bestAttemptContent.body = "Error decryption for \(tableAccount.account)" + continue + } + guard let data = decryptedMessage.data(using: .utf8) else { + bestAttemptContent.body = "Error decryption data utf8 for \(tableAccount.account)" + continue + } + + if var json = try JSONSerialization.jsonObject(with: data) as? [String: AnyObject], + let subject = json["subject"] as? String { + bestAttemptContent.body = subject + if let pref = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) { + json["account"] = tableAccount.account as AnyObject + pref.set(json, forKey: "NOTIFICATION_DATA") + pref.synchronize() } - break + + matchedAccount = tableAccount + payload = json + } else { + bestAttemptContent.body = "Error JSON Serialization for \(tableAccount.account)" } + break } - } catch let error as NSError { - nkLog(error: "Failed : \(error.localizedDescription)") } + } catch let error as NSError { + nkLog(error: "Failed : \(error.localizedDescription)") + } - contentHandler(bestAttemptContent) + guard let tblAccount = matchedAccount, + let nid = payload?["nid"] as? Int, + payload?["delete"] as? Bool != true, + payload?["delete-all"] as? Bool != true else { + deliver() + return + } + + NextcloudKit.shared.setup(groupIdentifier: NCBrandOptions.shared.capabilitiesGroup, delegate: NCNetworking.shared, memoryCapacity: 2, diskCapacity: 10) + NextcloudKit.shared.appendSession(account: tblAccount.account, + urlBase: tblAccount.urlBase, + user: tblAccount.user, + userId: tblAccount.userId, + password: NCPreferences().getPassword(account: tblAccount.account), + userAgent: userAgent, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) + + Task { + let results = await NextcloudKit.shared.getNotificationsAsync(idNotification: nid, + account: tblAccount.account, + options: NKRequestOptions(timeout: 15)) + + if results.error == .success, let notification = results.notifications?.first { + bestAttemptContent.title = notification.message.isEmpty ? NCBrandOptions.shared.brand : notification.subject + bestAttemptContent.body = notification.message.isEmpty ? notification.subject : notification.message + } + + deliver() } } override func serviceExtensionTimeWillExpire() { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { - bestAttemptContent.title = "" - bestAttemptContent.body = "Nextcloud Notification Time Will Expire" - contentHandler(bestAttemptContent) + deliver() + } + + private func deliver() { + let handler = deliveryLock.withLock { + let handler = contentHandler + contentHandler = nil + return handler + } + + if let handler, let bestAttemptContent { + handler(bestAttemptContent) } } } From eed3da23b1613c8e6b036bfa3937f7b0624d4e7b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 9 Jul 2026 13:50:28 +0200 Subject: [PATCH 2/3] WIP Signed-off-by: Milen Pivchev --- Notification Service Extension/NotificationService.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index af769aafdb..4aabbdfa52 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -95,7 +95,8 @@ class NotificationService: UNNotificationServiceExtension { return } - NextcloudKit.shared.setup(groupIdentifier: NCBrandOptions.shared.capabilitiesGroup, delegate: NCNetworking.shared, memoryCapacity: 2, diskCapacity: 10) + // NextcloudKit Session + NextcloudKit.shared.setup(groupIdentifier: NCBrandOptions.shared.capabilitiesGroup, delegate: NCNetworking.shared) NextcloudKit.shared.appendSession(account: tblAccount.account, urlBase: tblAccount.urlBase, user: tblAccount.user, @@ -107,7 +108,7 @@ class NotificationService: UNNotificationServiceExtension { Task { let results = await NextcloudKit.shared.getNotificationsAsync(idNotification: nid, account: tblAccount.account, - options: NKRequestOptions(timeout: 15)) + options: NKRequestOptions(timeout: 20)) if results.error == .success, let notification = results.notifications?.first { bestAttemptContent.title = notification.message.isEmpty ? NCBrandOptions.shared.brand : notification.subject From ffcd0028cd65d27c4c83d0b8a04a058d7c17504d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 13 Jul 2026 15:29:32 +0200 Subject: [PATCH 3/3] Refactor Signed-off-by: Milen Pivchev --- Notification Service Extension/NotificationService.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index 4aabbdfa52..b96ea03676 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -58,11 +58,11 @@ class NotificationService: UNNotificationServiceExtension { nkLog(debug: "🔑 Loaded private key for \(tableAccount.account): prefix(Base64)=\(prefixBase64)") guard let decryptedMessage = NCPushNotificationEncryption.shared().decryptPushNotification(message, withDevicePrivateKey: privateKey) else { - bestAttemptContent.body = "Error decryption for \(tableAccount.account)" + bestAttemptContent.body = "Error decrypting notification for \(tableAccount.account)" continue } guard let data = decryptedMessage.data(using: .utf8) else { - bestAttemptContent.body = "Error decryption data utf8 for \(tableAccount.account)" + bestAttemptContent.body = "Error decrypting UTF8 notification data for \(tableAccount.account)" continue } @@ -78,7 +78,7 @@ class NotificationService: UNNotificationServiceExtension { matchedAccount = tableAccount payload = json } else { - bestAttemptContent.body = "Error JSON Serialization for \(tableAccount.account)" + bestAttemptContent.body = "Error with notification JSON for \(tableAccount.account)" } break }