// // NotificationService.swift // NotificationService // // Created by MainasuK Cirno on 2021-4-23. // import UserNotifications import CommonOSLog import CryptoKit import AlamofireImage import MastodonCore class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { // Modify the notification content here... os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let privateKey = AppSecret.default.notificationPrivateKey let auth = AppSecret.default.notificationAuth guard let encodedPayload = bestAttemptContent.userInfo["p"] as? String else { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid payload", ((#file as NSString).lastPathComponent), #line, #function) contentHandler(bestAttemptContent) return } let payload = encodedPayload.decode85() guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, let publicKey = NotificationService.publicKey(encodedPublicKey: encodedPublicKey) else { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid public key", ((#file as NSString).lastPathComponent), #line, #function) contentHandler(bestAttemptContent) return } guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String else { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid salt", ((#file as NSString).lastPathComponent), #line, #function) contentHandler(bestAttemptContent) return } let salt = encodedSalt.decode85() guard let plaintextData = NotificationService.decrypt(payload: payload, salt: salt, auth: auth, privateKey: privateKey, publicKey: publicKey), let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else { contentHandler(bestAttemptContent) return } bestAttemptContent.title = notification.title bestAttemptContent.subtitle = "" bestAttemptContent.body = notification.body.escape() bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "BoopSound.caf")) bestAttemptContent.userInfo["plaintext"] = plaintextData let accessToken = notification.accessToken UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) UserDefaults.shared.notificationBadgeCount += 1 bestAttemptContent.badge = NSNumber(integerLiteral: UserDefaults.shared.notificationBadgeCount) if let urlString = notification.icon, let url = URL(string: urlString) { let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments") try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) let filename = url.lastPathComponent let fileURL = temporaryDirectoryURL.appendingPathComponent(filename) ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in guard let _ = self else { return } switch response.result { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) case .success(let image): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) try? image.pngData()?.write(to: fileURL) if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) { bestAttemptContent.attachments = [attachment] } } contentHandler(bestAttemptContent) }) } else { contentHandler(bestAttemptContent) } } } 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 { contentHandler(bestAttemptContent) } } } extension NotificationService { static func publicKey(encodedPublicKey: String) -> P256.KeyAgreement.PublicKey? { let publicKeyData = encodedPublicKey.decode85() return try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) } } 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: "’") } }