diff --git a/Bark.xcodeproj/project.pbxproj b/Bark.xcodeproj/project.pbxproj index 897fd71..6630de6 100644 --- a/Bark.xcodeproj/project.pbxproj +++ b/Bark.xcodeproj/project.pbxproj @@ -126,6 +126,17 @@ 06C595362481160F006B98F3 /* BKLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C595352481160F006B98F3 /* BKLabel.swift */; }; 06CF784721C7A50300A052D7 /* NotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CF784B21C7A51200A052D7 /* NotificationService.swift */; }; + 06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */; }; + 06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */; }; + 06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */; }; + 06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */; }; + 06E944712C06FF4C00AC86AB /* AutoCopyProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */; }; + 06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */; }; + 06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944742C07012E00AC86AB /* RealmConfiguration.swift */; }; + 06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944742C07012E00AC86AB /* RealmConfiguration.swift */; }; + 06E944782C0701F300AC86AB /* IconProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944772C0701F300AC86AB /* IconProcessor.swift */; }; + 06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944792C0704E500AC86AB /* ImageDownloader.swift */; }; + 06E9447C2C07052F00AC86AB /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9447B2C07052F00AC86AB /* ImageProcessor.swift */; }; 06EE1FD326843E9300586708 /* BarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EE1FD226843E9300586708 /* BarkTests.swift */; }; 06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF332291CCFF400CA228A /* CryptoSettingController.swift */; }; 06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */; }; @@ -305,6 +316,16 @@ 06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 06CF784421C7A50300A052D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 06CF784B21C7A51200A052D7 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentProcessor.swift; sourceTree = ""; }; + 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CiphertextProcessor.swift; sourceTree = ""; }; + 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProcessor.swift; sourceTree = ""; }; + 06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeProcessor.swift; sourceTree = ""; }; + 06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCopyProcessor.swift; sourceTree = ""; }; + 06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveProcessor.swift; sourceTree = ""; }; + 06E944742C07012E00AC86AB /* RealmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmConfiguration.swift; sourceTree = ""; }; + 06E944772C0701F300AC86AB /* IconProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconProcessor.swift; sourceTree = ""; }; + 06E944792C0704E500AC86AB /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; + 06E9447B2C07052F00AC86AB /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = ""; }; 06EE1FD026843E9300586708 /* BarkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BarkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 06EE1FD226843E9300586708 /* BarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkTests.swift; sourceTree = ""; }; 06EE1FD426843E9300586708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -512,6 +533,7 @@ 0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */, 06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */, 06F08EAB29B1DECD006AB9CA /* NSLocalizedString+Extension.swift */, + 06E944742C07012E00AC86AB /* RealmConfiguration.swift */, ); path = Common; sourceTree = ""; @@ -571,6 +593,7 @@ 06CF784121C7A50300A052D7 /* NotificationServiceExtension */ = { isa = PBXGroup; children = ( + 06E9446B2C06E4A800AC86AB /* Processor */, 06B11590247BBC15006D91FB /* NotificationServiceExtension.entitlements */, 06CF784B21C7A51200A052D7 /* NotificationService.swift */, 06CF784421C7A50300A052D7 /* Info.plist */, @@ -578,6 +601,22 @@ path = NotificationServiceExtension; sourceTree = ""; }; + 06E9446B2C06E4A800AC86AB /* Processor */ = { + isa = PBXGroup; + children = ( + 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */, + 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */, + 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */, + 06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */, + 06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */, + 06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */, + 06E944772C0701F300AC86AB /* IconProcessor.swift */, + 06E9447B2C07052F00AC86AB /* ImageProcessor.swift */, + 06E944792C0704E500AC86AB /* ImageDownloader.swift */, + ); + path = Processor; + sourceTree = ""; + }; 06EE1FD126843E9300586708 /* BarkTests */ = { isa = PBXGroup; children = ( @@ -981,6 +1020,7 @@ 0667D194247D1BA0005DE2ED /* Date+Extension.swift in Sources */, 0604F7DF20620D4900B32F09 /* ServerManager.swift in Sources */, 0667D192247D162C005DE2ED /* MessageTableViewCell.swift in Sources */, + 06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */, 0603706720E1E31600F4CA05 /* Defines.swift in Sources */, 06787C3B2AB82BDB008ABDD7 /* CrashReportViewController.swift in Sources */, 064CAB9E256BE9090018155C /* PreviewCardCellViewModel.swift in Sources */, @@ -1015,12 +1055,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */, + 06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */, + 06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */, 06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */, 06F08EAD29B1DED6006AB9CA /* NSLocalizedString+Extension.swift in Sources */, 0653677629B719BC0038BDB8 /* CryptoSettingManager.swift in Sources */, + 06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */, + 06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */, + 06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */, 06F08EA529B1DDA7006AB9CA /* Algorithm.swift in Sources */, + 06E944782C0701F300AC86AB /* IconProcessor.swift in Sources */, 06BBB89125650CCF0076F63E /* ArchiveSettingManager.swift in Sources */, 06B11591247BC132006D91FB /* Message.swift in Sources */, + 06E9447C2C07052F00AC86AB /* ImageProcessor.swift in Sources */, + 06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */, + 06E944712C06FF4C00AC86AB /* AutoCopyProcessor.swift in Sources */, 06F08EA829B1DE0A006AB9CA /* Error+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Bark/AppDelegate.swift b/Bark/AppDelegate.swift index feb7879..19d4702 100644 --- a/Bark/AppDelegate.swift +++ b/Bark/AppDelegate.swift @@ -11,7 +11,6 @@ import CrashReporter import IceCream import IQKeyboardManagerSwift import Material -import RealmSwift import UIKit import UserNotifications @@ -20,22 +19,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var window: UIWindow? var syncEngine: SyncEngine? func setupRealm() { - let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark") - let fileUrl = groupUrl?.appendingPathComponent("bark.realm") - let config = Realm.Configuration( - fileURL: fileUrl, - schemaVersion: 13, - migrationBlock: { _, oldSchemaVersion in - // We haven’t migrated anything yet, so oldSchemaVersion == 0 - if oldSchemaVersion < 1 { - // Nothing to do! - // Realm will automatically detect new properties and removed properties - // And will update the schema on disk automatically - } - } - ) // Tell Realm to use this new configuration object for the default Realm - Realm.Configuration.defaultConfiguration = config + Realm.Configuration.defaultConfiguration = kRealmDefaultConfiguration // iCloud 同步 syncEngine = SyncEngine(objects: [ diff --git a/Common/RealmConfiguration.swift b/Common/RealmConfiguration.swift new file mode 100644 index 0000000..50a283f --- /dev/null +++ b/Common/RealmConfiguration.swift @@ -0,0 +1,28 @@ +// +// RealmConfiguration.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +@_exported import RealmSwift +import UIKit + +let kRealmDefaultConfiguration = { + let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark") + let fileUrl = groupUrl?.appendingPathComponent("bark.realm") + let config = Realm.Configuration( + fileURL: fileUrl, + schemaVersion: 13, + migrationBlock: { _, oldSchemaVersion in + // We haven’t migrated anything yet, so oldSchemaVersion == 0 + if oldSchemaVersion < 1 { + // Nothing to do! + // Realm will automatically detect new properties and removed properties + // And will update the schema on disk automatically + } + } + ) + return config +}() diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index 095eea2..e1e977f 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -6,334 +6,36 @@ // Copyright © 2018 Fin. All rights reserved. // -import Intents -import Kingfisher -import MobileCoreServices -import RealmSwift -import SwiftyJSON -import UIKit import UserNotifications class NotificationService: UNNotificationServiceExtension { - - lazy var realm: Realm? = { - let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark") - let fileUrl = groupUrl?.appendingPathComponent("bark.realm") - let config = Realm.Configuration( - fileURL: fileUrl, - schemaVersion: 13, - migrationBlock: { _, oldSchemaVersion in - // We haven’t migrated anything yet, so oldSchemaVersion == 0 - if oldSchemaVersion < 1 { - // Nothing to do! - // Realm will automatically detect new properties and removed properties - // And will update the schema on disk automatically - } - } - ) - - // Tell Realm to use this new configuration object for the default Realm - Realm.Configuration.defaultConfiguration = config - return try? Realm() - }() - - /// 自动保存推送 - /// - Parameters: - /// - userInfo: 推送参数 - /// - bestAttemptContentBody: 推送body,如果用户`没有指定要复制的值` ,默认复制 `推送正文` - fileprivate func autoCopy(_ userInfo: [AnyHashable: Any], defaultCopy: String) { - if userInfo["autocopy"] as? String == "1" - || userInfo["automaticallycopy"] as? String == "1" - { - if let copy = userInfo["copy"] as? String { - UIPasteboard.general.string = copy - } - else { - UIPasteboard.general.string = defaultCopy - } - } - } - - /// 保存推送 - /// - Parameter userInfo: 推送参数 - /// 如果用户携带了 `isarchive` 参数,则以 `isarchive` 参数值为准 - /// 否则,以用户`应用内设置`为准 - fileprivate func archive(_ userInfo: [AnyHashable: Any]) { - var isArchive: Bool? - if let archive = userInfo["isarchive"] as? String { - isArchive = archive == "1" ? true : false - } - if isArchive == nil { - isArchive = ArchiveSettingManager.shared.isArchive - } - let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any] - let title = alert?["title"] as? String - let body = alert?["body"] as? String - - let url = userInfo["url"] as? String - let group = userInfo["group"] as? String - - if isArchive == true { - try? realm?.write { - let message = Message() - message.title = title - message.body = body - message.url = url - message.group = group - message.createDate = Date() - realm?.add(message) - } - } - } - - /// 保存图片到缓存中 - /// - Parameters: - /// - cache: 使用的缓存 - /// - data: 图片 Data 数据 - /// - key: 缓存 Key - func storeImage(cache: ImageCache, data: Data, key: String) async { - return await withCheckedContinuation { continuation in - cache.storeToDisk(data, forKey: key, expiration: StorageExpiration.never) { _ in - continuation.resume() - } - } - } - - /// 使用 Kingfisher.ImageDownloader 下载图片 - /// - Parameter url: 下载的图片URL - /// - Returns: 返回 Result - func downloadImage(url: URL) async -> Result { - return await withCheckedContinuation { continuation in - Kingfisher.ImageDownloader.default.downloadImage(with: url, options: nil) { result in - continuation.resume(returning: result) - } - } - } - - /// 下载推送图片 - /// - Parameter imageUrl: 图片URL字符串 - /// - Returns: 保存在本地中的`图片 File URL` - fileprivate func downloadImage(_ imageUrl: String) async -> String? { - guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark"), - let cache = try? ImageCache(name: "shared", cacheDirectoryURL: groupUrl), - let imageResource = URL(string: imageUrl) - else { - return nil - } - - // 先查看图片缓存 - if cache.diskStorage.isCached(forKey: imageResource.cacheKey) { - return cache.cachePath(forKey: imageResource.cacheKey) - } - - // 下载图片 - guard let result = try? await downloadImage(url: imageResource).get() else { - return nil - } - // 缓存图片 - await storeImage(cache: cache, data: result.originalData, key: imageResource.cacheKey) - - return cache.cachePath(forKey: imageResource.cacheKey) - } - - /// 为 Notification Content 设置图片 - /// - Parameter bestAttemptContent: 要设置的 Notification Content - /// - Returns: 返回设置图片后的 Notification Content - fileprivate func setImage(content bestAttemptContent: UNMutableNotificationContent) async -> UNMutableNotificationContent { - let userInfo = bestAttemptContent.userInfo - guard let imageUrl = userInfo["image"] as? String, - let imageFileUrl = await downloadImage(imageUrl) - else { - return bestAttemptContent - } - - let copyDestUrl = URL(fileURLWithPath: imageFileUrl).appendingPathExtension(".tmp") - // 将图片缓存复制一份,推送使用完后会自动删除,但图片缓存需要留着以后在历史记录里查看 - try? FileManager.default.copyItem( - at: URL(fileURLWithPath: imageFileUrl), - to: copyDestUrl - ) - - if let attachment = try? UNNotificationAttachment( - identifier: "image", - url: copyDestUrl, - options: [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG] - ) { - bestAttemptContent.attachments = [attachment] - } - return bestAttemptContent - } - - /// 为 Notification Content 设置ICON - /// - Parameter bestAttemptContent: 要设置的 Notification Content - /// - Returns: 返回设置ICON后的 Notification Content - fileprivate func setIcon(content bestAttemptContent: UNMutableNotificationContent) async -> UNMutableNotificationContent { - if #available(iOSApplicationExtension 15.0, *) { - - let userInfo = bestAttemptContent.userInfo - - guard let imageUrl = userInfo["icon"] as? String, - let imageFileUrl = await downloadImage(imageUrl) - else { - return bestAttemptContent - } - - var personNameComponents = PersonNameComponents() - personNameComponents.nickname = bestAttemptContent.title - - let avatar = INImage(imageData: NSData(contentsOfFile: imageFileUrl)! as Data) - let senderPerson = INPerson( - personHandle: INPersonHandle(value: "", type: .unknown), - nameComponents: personNameComponents, - displayName: personNameComponents.nickname, - image: avatar, - contactIdentifier: nil, - customIdentifier: nil, - isMe: false, - suggestionType: .none - ) - let mePerson = INPerson( - personHandle: INPersonHandle(value: "", type: .unknown), - nameComponents: nil, - displayName: nil, - image: nil, - contactIdentifier: nil, - customIdentifier: nil, - isMe: true, - suggestionType: .none - ) - - let intent = INSendMessageIntent( - recipients: [mePerson], - outgoingMessageType: .outgoingMessageText, - content: bestAttemptContent.body, - speakableGroupName: INSpeakableString(spokenPhrase: personNameComponents.nickname ?? ""), - conversationIdentifier: bestAttemptContent.threadIdentifier, - serviceName: nil, - sender: senderPerson, - attachments: nil - ) - - intent.setImage(avatar, forParameterNamed: \.sender) - - let interaction = INInteraction(intent: intent, response: nil) - interaction.direction = .incoming - - try? await interaction.donate() - - do { - let content = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent - return content - } - catch {} - - return bestAttemptContent - } - else { - return bestAttemptContent - } - } - - func decrypt(ciphertext: String, iv: String? = nil) throws -> [String: Any] { - guard var fields = CryptoSettingManager.shared.fields else { - throw "No encryption key set" - } - if let iv = iv { - // Support using specified IV parameter for decryption - fields.iv = iv - } - - let aes = try AESCryptoModel(cryptoFields: fields) - - let json = try aes.decrypt(ciphertext: ciphertext) - - guard let data = json.data(using: .utf8), let map = JSON(data).dictionaryObject else { - throw "JSON parsing failed" - } - return map - } - - override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> ()) { - guard let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) else { - contentHandler(request.content) - return - } - - var userInfo = bestAttemptContent.userInfo - // 如果是加密推送,则使用密文配置 bestAttemptContent - if let ciphertext = userInfo["ciphertext"] as? String { - do { - var map = try decrypt(ciphertext: ciphertext, iv: userInfo["iv"] as? String) - for (key, val) in map { - // 将key重写为小写 - map[key.lowercased()] = val - } - - var alert = [String: Any]() - if let title = map["title"] as? String { - bestAttemptContent.title = title - alert["title"] = title - } - if let body = map["body"] as? String { - bestAttemptContent.body = body - alert["body"] = body - } - if let group = map["group"] as? String { - bestAttemptContent.threadIdentifier = group - } - if var sound = map["sound"] as? String { - if !sound.hasSuffix(".caf") { - sound = "\(sound).caf" - } - bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound)) - } - if let badge = map["badge"] as? Int { - bestAttemptContent.badge = badge as NSNumber - } - - map["aps"] = ["alert": alert] - userInfo = map - bestAttemptContent.userInfo = userInfo - } - catch { - bestAttemptContent.body = "Decryption Failed" - bestAttemptContent.userInfo = ["aps": ["alert": ["body": bestAttemptContent.body]]] - contentHandler(bestAttemptContent) + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + Task { + guard var bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) else { + contentHandler(request.content) return } - } - - // 通知中断级别 - if #available(iOSApplicationExtension 15.0, *) { - if let level = userInfo["level"] as? String { - let interruptionLevels: [String: UNNotificationInterruptionLevel] = [ - "passive": UNNotificationInterruptionLevel.passive, - "active": UNNotificationInterruptionLevel.active, - "timeSensitive": UNNotificationInterruptionLevel.timeSensitive, - "timesensitive": UNNotificationInterruptionLevel.timeSensitive, - "critical": UNNotificationInterruptionLevel.critical, - ] - bestAttemptContent.interruptionLevel = interruptionLevels[level] ?? .active + + let processors: [NotificationContentProcessorItem] = [ + .ciphertext, + .level, + .badge, + .autoCopy, + .archive, + .setIcon, + .setImage + ] + + for item in processors { + do { + bestAttemptContent = try await item.processor.process(content: bestAttemptContent) + } catch NotificationContentProcessorError.error(let content) { + contentHandler(content) + return + } } - } - - // 通知角标 - if let badgeStr = userInfo["badge"] as? String, let badge = Int(badgeStr) { - bestAttemptContent.badge = NSNumber(value: badge) - } - - // 自动复制 - autoCopy(userInfo, defaultCopy: bestAttemptContent.body) - - // 保存推送 - archive(userInfo) - - Task.init { - // 设置推送图标 - let iconResult = await setIcon(content: bestAttemptContent) - // 设置推送图片 - let imageResult = await self.setImage(content: iconResult) - contentHandler(imageResult) + + contentHandler(bestAttemptContent) } } } diff --git a/NotificationServiceExtension/Processor/ArchiveProcessor.swift b/NotificationServiceExtension/Processor/ArchiveProcessor.swift new file mode 100644 index 0000000..fdafed2 --- /dev/null +++ b/NotificationServiceExtension/Processor/ArchiveProcessor.swift @@ -0,0 +1,45 @@ +// +// ArchiveProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation +import RealmSwift + +class ArchiveProcessor: NotificationContentProcessor { + private lazy var realm: Realm? = { + Realm.Configuration.defaultConfiguration = kRealmDefaultConfiguration + return try? Realm() + }() + + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + let userInfo = bestAttemptContent.userInfo + + var isArchive: Bool = ArchiveSettingManager.shared.isArchive + if let archive = userInfo["isarchive"] as? String { + isArchive = archive == "1" ? true : false + } + + if isArchive { + let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any] + let title = alert?["title"] as? String + let body = alert?["body"] as? String + let url = userInfo["url"] as? String + let group = userInfo["group"] as? String + + try? realm?.write { + let message = Message() + message.title = title + message.body = body + message.url = url + message.group = group + message.createDate = Date() + realm?.add(message) + } + } + return bestAttemptContent + } +} diff --git a/NotificationServiceExtension/Processor/AutoCopyProcessor.swift b/NotificationServiceExtension/Processor/AutoCopyProcessor.swift new file mode 100644 index 0000000..be9f99e --- /dev/null +++ b/NotificationServiceExtension/Processor/AutoCopyProcessor.swift @@ -0,0 +1,25 @@ +// +// AutoCopyProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation + +class AutoCopyProcessor: NotificationContentProcessor { + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + let userInfo = bestAttemptContent.userInfo + if userInfo["autocopy"] as? String == "1" + || userInfo["automaticallycopy"] as? String == "1" + { + if let copy = userInfo["copy"] as? String { + UIPasteboard.general.string = copy + } else { + UIPasteboard.general.string = bestAttemptContent.body + } + } + return bestAttemptContent + } +} diff --git a/NotificationServiceExtension/Processor/BadgeProcessor.swift b/NotificationServiceExtension/Processor/BadgeProcessor.swift new file mode 100644 index 0000000..b7d80d7 --- /dev/null +++ b/NotificationServiceExtension/Processor/BadgeProcessor.swift @@ -0,0 +1,19 @@ +// +// BadgeProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation + +/// 通知角标 +class BadgeProcessor: NotificationContentProcessor { + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + if let badgeStr = bestAttemptContent.userInfo["badge"] as? String, let badge = Int(badgeStr) { + bestAttemptContent.badge = NSNumber(value: badge) + } + return bestAttemptContent + } +} diff --git a/NotificationServiceExtension/Processor/CiphertextProcessor.swift b/NotificationServiceExtension/Processor/CiphertextProcessor.swift new file mode 100644 index 0000000..e47ce6c --- /dev/null +++ b/NotificationServiceExtension/Processor/CiphertextProcessor.swift @@ -0,0 +1,86 @@ +// +// CiphertextProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation +import SwiftyJSON + +/// 加密推送 +class CiphertextProcessor: NotificationContentProcessor { + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + var userInfo = bestAttemptContent.userInfo + guard let ciphertext = userInfo["ciphertext"] as? String else { + return bestAttemptContent + } + + // 如果是加密推送,则使用密文配置 bestAttemptContent + do { + var map = try decrypt(ciphertext: ciphertext, iv: userInfo["iv"] as? String) + + var alert = [String: Any]() + if let title = map["title"] as? String { + bestAttemptContent.title = title + alert["title"] = title + } + if let body = map["body"] as? String { + bestAttemptContent.body = body + alert["body"] = body + } + if let group = map["group"] as? String { + bestAttemptContent.threadIdentifier = group + } + if var sound = map["sound"] as? String { + if !sound.hasSuffix(".caf") { + sound = "\(sound).caf" + } + bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound)) + } + if let badge = map["badge"] as? Int { + bestAttemptContent.badge = badge as NSNumber + } + + map["aps"] = ["alert": alert] + userInfo = map + bestAttemptContent.userInfo = userInfo + return bestAttemptContent + } catch { + bestAttemptContent.body = "Decryption Failed" + bestAttemptContent.userInfo = ["aps": ["alert": ["body": bestAttemptContent.body]]] + throw NotificationContentProcessorError.error(content: bestAttemptContent) + } + } + + /// 解密文本 + /// - Parameters: + /// - ciphertext: 密文 + /// - iv: iv 如果不传就用配置保存的,传了就以传的 iv 为准 + /// - Returns: 解密后的 json 数据 + private func decrypt(ciphertext: String, iv: String? = nil) throws -> [AnyHashable: Any] { + guard var fields = CryptoSettingManager.shared.fields else { + throw "No encryption key set" + } + if let iv = iv { + // Support using specified IV parameter for decryption + fields.iv = iv + } + + let aes = try AESCryptoModel(cryptoFields: fields) + + let json = try aes.decrypt(ciphertext: ciphertext) + + guard let data = json.data(using: .utf8), let map = JSON(data).dictionaryObject else { + throw "JSON parsing failed" + } + + var result: [AnyHashable: Any] = [:] + for (key, val) in map { + // 将key重写为小写 + result[key.lowercased()] = val + } + return result + } +} diff --git a/NotificationServiceExtension/Processor/IconProcessor.swift b/NotificationServiceExtension/Processor/IconProcessor.swift new file mode 100644 index 0000000..b976ad5 --- /dev/null +++ b/NotificationServiceExtension/Processor/IconProcessor.swift @@ -0,0 +1,75 @@ +// +// IconProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation +import Intents + +class IconProcessor: NotificationContentProcessor { + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + if #available(iOSApplicationExtension 15.0, *) { + let userInfo = bestAttemptContent.userInfo + + guard let imageUrl = userInfo["icon"] as? String, + let imageFileUrl = await ImageDownloader.downloadImage(imageUrl) + else { + return bestAttemptContent + } + + var personNameComponents = PersonNameComponents() + personNameComponents.nickname = bestAttemptContent.title + + let avatar = INImage(imageData: NSData(contentsOfFile: imageFileUrl)! as Data) + let senderPerson = INPerson( + personHandle: INPersonHandle(value: "", type: .unknown), + nameComponents: personNameComponents, + displayName: personNameComponents.nickname, + image: avatar, + contactIdentifier: nil, + customIdentifier: nil, + isMe: false, + suggestionType: .none + ) + let mePerson = INPerson( + personHandle: INPersonHandle(value: "", type: .unknown), + nameComponents: nil, + displayName: nil, + image: nil, + contactIdentifier: nil, + customIdentifier: nil, + isMe: true, + suggestionType: .none + ) + + let intent = INSendMessageIntent( + recipients: [mePerson], + outgoingMessageType: .outgoingMessageText, + content: bestAttemptContent.body, + speakableGroupName: INSpeakableString(spokenPhrase: personNameComponents.nickname ?? ""), + conversationIdentifier: bestAttemptContent.threadIdentifier, + serviceName: nil, + sender: senderPerson, + attachments: nil + ) + + intent.setImage(avatar, forParameterNamed: \.sender) + + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .incoming + + do { + try await interaction.donate() + let content = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent + return content + } catch { + return bestAttemptContent + } + } else { + return bestAttemptContent + } + } +} diff --git a/NotificationServiceExtension/Processor/ImageDownloader.swift b/NotificationServiceExtension/Processor/ImageDownloader.swift new file mode 100644 index 0000000..721d718 --- /dev/null +++ b/NotificationServiceExtension/Processor/ImageDownloader.swift @@ -0,0 +1,62 @@ +// +// ImageDownloader.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation +import Kingfisher + +class ImageDownloader { + /// 保存图片到缓存中 + /// - Parameters: + /// - cache: 使用的缓存 + /// - data: 图片 Data 数据 + /// - key: 缓存 Key + class func storeImage(cache: ImageCache, data: Data, key: String) async { + return await withCheckedContinuation { continuation in + cache.storeToDisk(data, forKey: key, expiration: StorageExpiration.never) { _ in + continuation.resume() + } + } + } + + /// 使用 Kingfisher.ImageDownloader 下载图片 + /// - Parameter url: 下载的图片URL + /// - Returns: 返回 Result + class func downloadImage(url: URL) async -> Result { + return await withCheckedContinuation { continuation in + Kingfisher.ImageDownloader.default.downloadImage(with: url, options: nil) { result in + continuation.resume(returning: result) + } + } + } + + /// 下载推送图片 + /// - Parameter imageUrl: 图片URL字符串 + /// - Returns: 保存在本地中的`图片 File URL` + class func downloadImage(_ imageUrl: String) async -> String? { + guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark"), + let cache = try? ImageCache(name: "shared", cacheDirectoryURL: groupUrl), + let imageResource = URL(string: imageUrl) + else { + return nil + } + + // 先查看图片缓存 + if cache.diskStorage.isCached(forKey: imageResource.cacheKey) { + return cache.cachePath(forKey: imageResource.cacheKey) + } + + // 下载图片 + guard let result = try? await downloadImage(url: imageResource).get() else { + return nil + } + // 缓存图片 + await storeImage(cache: cache, data: result.originalData, key: imageResource.cacheKey) + + return cache.cachePath(forKey: imageResource.cacheKey) + } +} diff --git a/NotificationServiceExtension/Processor/ImageProcessor.swift b/NotificationServiceExtension/Processor/ImageProcessor.swift new file mode 100644 index 0000000..e87bd3d --- /dev/null +++ b/NotificationServiceExtension/Processor/ImageProcessor.swift @@ -0,0 +1,37 @@ +// +// ImageProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation +import MobileCoreServices + +class ImageProcessor: NotificationContentProcessor { + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + let userInfo = bestAttemptContent.userInfo + guard let imageUrl = userInfo["image"] as? String, + let imageFileUrl = await ImageDownloader.downloadImage(imageUrl) + else { + return bestAttemptContent + } + + let copyDestUrl = URL(fileURLWithPath: imageFileUrl).appendingPathExtension(".tmp") + // 将图片缓存复制一份,推送使用完后会自动删除,但图片缓存需要留着以后在历史记录里查看 + try? FileManager.default.copyItem( + at: URL(fileURLWithPath: imageFileUrl), + to: copyDestUrl + ) + + if let attachment = try? UNNotificationAttachment( + identifier: "image", + url: copyDestUrl, + options: [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG] + ) { + bestAttemptContent.attachments = [attachment] + } + return bestAttemptContent + } +} diff --git a/NotificationServiceExtension/Processor/LevelProcessor.swift b/NotificationServiceExtension/Processor/LevelProcessor.swift new file mode 100644 index 0000000..116ba41 --- /dev/null +++ b/NotificationServiceExtension/Processor/LevelProcessor.swift @@ -0,0 +1,28 @@ +// +// LevelProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation + +/// 通知中断级别 +class LevelProcessor: NotificationContentProcessor { + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { + if #available(iOSApplicationExtension 15.0, *) { + if let level = bestAttemptContent.userInfo["level"] as? String { + let interruptionLevels: [String: UNNotificationInterruptionLevel] = [ + "passive": UNNotificationInterruptionLevel.passive, + "active": UNNotificationInterruptionLevel.active, + "timeSensitive": UNNotificationInterruptionLevel.timeSensitive, + "timesensitive": UNNotificationInterruptionLevel.timeSensitive, + "critical": UNNotificationInterruptionLevel.critical + ] + bestAttemptContent.interruptionLevel = interruptionLevels[level] ?? .active + } + } + return bestAttemptContent + } +} diff --git a/NotificationServiceExtension/Processor/NotificationContentProcessor.swift b/NotificationServiceExtension/Processor/NotificationContentProcessor.swift new file mode 100644 index 0000000..a969127 --- /dev/null +++ b/NotificationServiceExtension/Processor/NotificationContentProcessor.swift @@ -0,0 +1,51 @@ +// +// NotificationContentProcessor.swift +// NotificationServiceExtension +// +// Created by huangfeng on 2024/5/29. +// Copyright © 2024 Fin. All rights reserved. +// + +import Foundation +@_exported import UserNotifications + +enum NotificationContentProcessorItem { + case ciphertext + case level + case badge + case autoCopy + case archive + case setIcon + case setImage + + var processor: NotificationContentProcessor { + switch self { + case .ciphertext: + return CiphertextProcessor() + case .level: + return LevelProcessor() + case .badge: + return BadgeProcessor() + case .autoCopy: + return AutoCopyProcessor() + case .archive: + return ArchiveProcessor() + case .setIcon: + return IconProcessor() + case .setImage: + return ImageProcessor() + } + } +} + +enum NotificationContentProcessorError: Swift.Error { + case error(content: UNMutableNotificationContent) +} + +public protocol NotificationContentProcessor { + /// 处理 UNMutableNotificationContent + /// - Parameter bestAttemptContent: 需要处理的 UNMutableNotificationContent + /// - Returns: 处理成功后的 UNMutableNotificationContent + /// - Throws: 处理失败后,应该中断处理 + func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent +} diff --git a/Podfile.lock b/Podfile.lock index e00f043..0fe9884 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -153,4 +153,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 73cfda85b7a345d896330d72bc61822356f98586 -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2