Bark/NotificationServiceExtension/NotificationService.swift
2023-03-15 16:57:17 +08:00

340 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// NotificationService.swift
// NotificationServiceExtension
//
// Created by huangfeng on 2018/12/17.
// 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 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)
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
}
}
//
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)
}
}
}