Harmonize Message and userInfo stuff

This commit is contained in:
Philipp Heckel 2022-05-26 12:56:25 -04:00
parent fd87316f4e
commit 379ed1bed1
7 changed files with 88 additions and 75 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
// Get all the things
let event = userInfo["event"] as? String ?? ""
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 ?? "[]"
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
}
// Only handle "message" events
if event != "message" {
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
let topic = userInfo["topic"] as? String ?? ""
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)
}
}