Browse Source

Harmonize Message and userInfo stuff

Philipp Heckel 2 years ago
parent
commit
379ed1bed1

+ 11 - 12
ntfy/App/AppDelegate.swift

@@ -3,6 +3,7 @@ import SafariServices
 import UserNotifications
 import Firebase
 import FirebaseCore
+import FirebaseMessaging
 import CoreData
 
 class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
@@ -86,6 +87,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
         content.sound = .default
         content.userInfo = userInfo
         
+        // FIXME: Use logic in NotificationService here to build the same message
+        
         let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */)
         UNUserNotificationCenter.current().add(request) { (error) in
             if let error = error {
@@ -114,15 +117,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
         withCompletionHandler completionHandler: @escaping () -> Void
     ) {
         let userInfo = response.notification.request.content.userInfo
-        let actionId = response.actionIdentifier
-
         Log.d(tag, "Notification received via userNotificationCenter(didReceive)", userInfo)
+        guard let message = Message.from(userInfo: userInfo) else {
+            Log.w(tag, "Cannot convert userInfo to message", userInfo)
+            completionHandler()
+            return
+        }
         
         let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
         let topic = userInfo["topic"] as? String ?? ""
-        let clickUrl = URL(string: userInfo["click"] as? String ?? "")
-        let actions = userInfo["actions"] as? String ?? "[]"
-        let action = findAction(id: actionId, actions: Actions.shared.parse(actions))
+        let action = message.actions?.first { $0.id == response.actionIdentifier }
 
         // Show current topic
         if topic != "" {
@@ -132,18 +136,13 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
         // Execute user action or click action (if any)
         if let action = action {
             handle(action: action)
-        } else if let clickUrl = clickUrl {
-            open(url: clickUrl)
+        } else if let click = message.click, let url = URL(string: click) {
+            open(url: url)
         }
     
         completionHandler()
     }
     
-    private func findAction(id: String, actions: [Action]?) -> Action? {
-        guard let actions = actions else { return nil }
-        return actions.first { $0.id == id }
-    }
-    
     private func handle(action: Action) {
         Log.d(tag, "Executing user action", action)
         switch action.action {

+ 30 - 6
ntfy/Persistence/Notification.swift

@@ -70,16 +70,12 @@ struct Message: Decodable {
     var priority: Int16?
     var tags: [String]?
     var actions: [Action]?
+    var click: String?
     
     func toUserInfo() -> [AnyHashable: Any] {
         // This should mimic the way that the ntfy server encodes a message.
         // See server_firebase.go for more details.
         
-        var actionsStr: String?
-        if let actionsData = try? JSONEncoder().encode(actions) {
-            actionsStr = String(data: actionsData, encoding: .utf8)
-        }
-        
         return [
             "id": id,
             "event": event,
@@ -88,9 +84,37 @@ struct Message: Decodable {
             "title": title ?? "",
             "priority": String(priority ?? 3),
             "tags": tags?.joined(separator: ",") ?? "",
-            "actions": actionsStr ?? ""
+            "actions": Actions.shared.encode(actions),
+            "click": click ?? ""
         ]
     }
+    
+    static func from(userInfo: [AnyHashable: Any]) -> Message? {
+        guard let id = userInfo["id"] as? String,
+              let time = userInfo["time"] as? String,
+              let event = userInfo["event"] as? String,
+              let timeInt = Int64(time),
+              let message = userInfo["message"] as? String else {
+            Log.d(Store.tag, "Unknown or irrelevant message", userInfo)
+            return nil
+        }
+        let title = userInfo["title"] as? String
+        let priority = Int16(userInfo["priority"] as? String ?? "3") ?? 3
+        let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
+        let actions = userInfo["actions"] as? String
+        let click = userInfo["click"] as? String
+        return Message(
+            id: id,
+            time: timeInt,
+            event: event,
+            message: message,
+            title: title,
+            priority: priority,
+            tags: tags,
+            actions: Actions.shared.parse(actions),
+            click: click
+        )
+    }
 }
 
 struct Action: Encodable, Decodable {

+ 2 - 32
ntfy/Persistence/Store.swift

@@ -79,37 +79,6 @@ class Store: ObservableObject {
         try? context.save()
     }
     
-    func save(notificationFromUserInfo userInfo: [AnyHashable: Any]) {
-        guard let id = userInfo["id"] as? String,
-              let time = userInfo["time"] as? String,
-              let event = userInfo["event"] as? String,
-              let topic = userInfo["topic"] as? String,
-              let timeInt = Int64(time),
-              let message = userInfo["message"] as? String else {
-            Log.d(Store.tag, "Unknown or irrelevant message", userInfo)
-            return
-        }
-        let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl // Firebase messages all come from the main ntfy server
-        guard let subscription = getSubscription(baseUrl: baseUrl, topic: topic) else {
-            Log.d(Store.tag, "Subscription for topic \(topic) unknown")
-            return
-        }
-        let title = userInfo["title"] as? String ?? ""
-        let priority = Int16(userInfo["priority"] as? String ?? "3") ?? 3
-        let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
-        let m = Message(
-            id: id,
-            time: timeInt,
-            event: event,
-            message: message,
-            title: title,
-            priority: priority,
-            tags: tags,
-            actions: nil // TODO: Actions
-        )
-        save(notificationFromMessage: m, withSubscription: subscription)
-    }
-    
     func save(notificationFromMessage message: Message, withSubscription subscription: Subscription) {
         do {
             let notification = Notification(context: context)
@@ -119,7 +88,8 @@ class Store: ObservableObject {
             notification.title = message.title ?? ""
             notification.priority = (message.priority != nil && message.priority != 0) ? message.priority! : 3
             notification.tags = message.tags?.joined(separator: ",") ?? ""
-            // TODO: actions
+            notification.actions = Actions.shared.encode(message.actions)
+            notification.click = message.click ?? ""
             subscription.addToNotifications(notification)
             subscription.lastNotificationId = message.id
             try context.save()

+ 3 - 1
ntfy/Persistence/ntfy.xcdatamodeld/Model.xcdatamodel/contents

@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E258" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
+        <attribute name="actions" optional="YES" attributeType="String"/>
+        <attribute name="click" optional="YES" attributeType="String"/>
         <attribute name="id" attributeType="String"/>
         <attribute name="message" attributeType="String"/>
         <attribute name="priority" optional="YES" attributeType="Integer 16" minValueString="1" maxValueString="5" defaultValueString="3" usesScalarValueType="YES"/>
@@ -27,7 +29,7 @@
         </uniquenessConstraints>
     </entity>
     <elements>
-        <element name="Notification" positionX="-54" positionY="9" width="128" height="134"/>
+        <element name="Notification" positionX="-54" positionY="9" width="128" height="164"/>
         <element name="Subscription" positionX="-262.4760131835938" positionY="11.46405029296875" width="128" height="89"/>
     </elements>
 </model>

+ 8 - 0
ntfy/Utils/Actions.swift

@@ -17,6 +17,14 @@ struct Actions {
         }
     }
     
+    func encode(_ actions: [Action]?) -> String {
+        guard let actions = actions else { return "" }
+        if let actionsData = try? JSONEncoder().encode(actions) {
+            return String(data: actionsData, encoding: .utf8) ?? ""
+        }
+        return ""
+    }
+    
     func http(_ action: Action) {
         guard let actionUrl = action.url, let url = URL(string: actionUrl) else {
             Log.w(tag, "Unable to execute HTTP action, no or invalid URL", action)

+ 7 - 2
ntfy/Utils/Helpers.swift

@@ -12,12 +12,17 @@ func topicShortUrl(baseUrl: String, topic: String) -> String {
 
 func parseAllTags(_ tags: String?) -> [String] {
     return (tags?.components(separatedBy: ",") ?? [])
-        .filter { $0.trimmingCharacters(in: [" "]) != "" }
+        .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
 }
 
 func parseEmojiTags(_ tags: String?) -> [String] {
+    return parseEmojiTags(parseAllTags(tags))
+}
+
+func parseEmojiTags(_ tags: [String]?) -> [String] {
+    guard let tags = tags else { return [] }
     var emojiTags: [String] = []
-    for tag in parseAllTags(tags) {
+    for tag in tags {
         if let emoji = EmojiManager.shared.getEmojiByAlias(alias: tag) {
             emojiTags.append(emoji.getUnicode())
         }

+ 25 - 20
ntfyNSE/NotificationService.swift

@@ -20,33 +20,38 @@ class NotificationService: UNNotificationServiceExtension {
         Log.d(tag, "Notification received (in service)") // Logs from extensions are not printed in Xcode!
 
         if let bestAttemptContent = bestAttemptContent {
+            let store = Store.shared
             let userInfo = bestAttemptContent.userInfo
+            guard let message = Message.from(userInfo: userInfo) else {
+                Log.w(Store.tag, "Message cannot be parsed from userInfo", userInfo)
+                contentHandler(request.content)
+                return
+            }
+            if message.event != "message" {
+                Log.w(tag, "Irrelevant message received", message)
+                contentHandler(request.content)
+                return
+            }
             
-            // Get all the things
-            let event = userInfo["event"]  as? String ?? ""
+            // Only handle "message" events
             let baseUrl = userInfo["base_url"]  as? String ?? Config.appBaseUrl
             let topic = userInfo["topic"]  as? String ?? ""
-            let title = userInfo["title"] as? String
-            let priority = userInfo["priority"] as? String ?? "3"
-            let tags = userInfo["tags"] as? String
-            let actions = userInfo["actions"] as? String ?? "[]"
-
-            // Only handle "message" events
-            if event != "message" {
+            guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: topic) else {
+                Log.w(tag, "Subscription for topic \(topic) unknown")
                 contentHandler(request.content)
                 return
             }
-            
+
             // Set notification title to short URL if there is no title. The title is always set
             // by the server, but it may be empty.
-            if let title = title, title == "" {
+            if let title = message.title, title == "" {
                 bestAttemptContent.title = topicShortUrl(baseUrl: baseUrl, topic: topic)
             }
             
             // Emojify title or message
-            let emojiTags = parseEmojiTags(tags)
+            let emojiTags = parseEmojiTags(message.tags)
             if !emojiTags.isEmpty {
-                if let title = title, title != "" {
+                if let title = message.title, title != "" {
                     bestAttemptContent.title = emojiTags.joined(separator: "") + " " + bestAttemptContent.title
                 } else {
                     bestAttemptContent.body = emojiTags.joined(separator: "") + " " + bestAttemptContent.body
@@ -61,7 +66,7 @@ class NotificationService: UNNotificationServiceExtension {
             //
             // We also must set the .foreground flag, which brings the notification to the foreground and avoids an error about
             // permissions. This is described in https://stackoverflow.com/a/44580916/1440785
-            if let actions = Actions.shared.parse(actions), !actions.isEmpty {
+            if let actions = message.actions, !actions.isEmpty {
                 bestAttemptContent.categoryIdentifier = actionsCategory
 
                 let center = UNUserNotificationCenter.current()
@@ -76,17 +81,17 @@ class NotificationService: UNNotificationServiceExtension {
 
             // Map priorities to interruption level (light up screen, ...) and relevance (order)
             if #available(iOS 15.0, *) {
-                switch priority {
-                case "1":
+                switch message.priority {
+                case 1:
                     bestAttemptContent.interruptionLevel = .passive
                     bestAttemptContent.relevanceScore = 0
-                case "2":
+                case 2:
                     bestAttemptContent.interruptionLevel = .passive
                     bestAttemptContent.relevanceScore = 0.25
-                case "4":
+                case 4:
                     bestAttemptContent.interruptionLevel = .timeSensitive
                     bestAttemptContent.relevanceScore = 0.75
-                case "5":
+                case 5:
                     bestAttemptContent.interruptionLevel = .critical
                     bestAttemptContent.relevanceScore = 1
                 default:
@@ -96,7 +101,7 @@ class NotificationService: UNNotificationServiceExtension {
             }
             
             // Save notification to store, and display it
-            Store.shared.save(notificationFromUserInfo: userInfo)
+            Store.shared.save(notificationFromMessage: message, withSubscription: subscription)
             contentHandler(bestAttemptContent)
         }
     }