重构NotificationService

This commit is contained in:
Fin 2024-05-29 15:14:12 +08:00
parent f509cbd5d4
commit 0238cacf5d
14 changed files with 532 additions and 339 deletions

View File

@ -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 = "<group>"; };
06CF784B21C7A51200A052D7 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentProcessor.swift; sourceTree = "<group>"; };
06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CiphertextProcessor.swift; sourceTree = "<group>"; };
06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProcessor.swift; sourceTree = "<group>"; };
06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeProcessor.swift; sourceTree = "<group>"; };
06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCopyProcessor.swift; sourceTree = "<group>"; };
06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveProcessor.swift; sourceTree = "<group>"; };
06E944742C07012E00AC86AB /* RealmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmConfiguration.swift; sourceTree = "<group>"; };
06E944772C0701F300AC86AB /* IconProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconProcessor.swift; sourceTree = "<group>"; };
06E944792C0704E500AC86AB /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
06E9447B2C07052F00AC86AB /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = "<group>"; };
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 = "<group>"; };
06EE1FD426843E9300586708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -512,6 +533,7 @@
0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */,
06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */,
06F08EAB29B1DECD006AB9CA /* NSLocalizedString+Extension.swift */,
06E944742C07012E00AC86AB /* RealmConfiguration.swift */,
);
path = Common;
sourceTree = "<group>";
@ -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 = "<group>";
};
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 = "<group>";
};
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;

View File

@ -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 havent 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: [

View File

@ -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 havent 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
}()

View File

@ -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 havent 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<ImageLoadingResult, KingfisherError> {
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)
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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<ImageLoadingResult, KingfisherError> {
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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -153,4 +153,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 73cfda85b7a345d896330d72bc61822356f98586
COCOAPODS: 1.11.3
COCOAPODS: 1.15.2