From 92c0da036de0312f408f2bab500a7ea10e1b5a03 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 27 May 2022 20:45:17 -0400 Subject: [PATCH] Horrible working prototype --- .gitignore | 1 + ntfy.xcodeproj/project.pbxproj | 2 ++ ntfy/Persistence/Notification.swift | 8 +++-- ntfy/Persistence/Subscription.swift | 6 +++- ntfy/Persistence/SubscriptionManager.swift | 14 ++++++-- ntfy/Utils/ApiService.swift | 23 +++++++++++++- ntfy/Utils/Helpers.swift | 7 ++++ ntfyNSE/NotificationService.swift | 37 ++++++++++++++++++++-- 8 files changed, 89 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index b71c2c4..3e8f0a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store GoogleService-Info.plist ntfy.xcodeproj/xcuserdata +ntfy.xcodeproj/project.xcworkspace/xcuserdata/ diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index ed5e92e..81bc00d 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 9474F217283531A300CDE4DD /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; 94867143283EC9960093C7A4 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94867142283EC9950093C7A4 /* Actions.swift */; }; 94867144283ECD370093C7A4 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94867142283EC9950093C7A4 /* Actions.swift */; }; + 94867145284058C60093C7A4 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20E283326C500CDE4DD /* ApiService.swift */; }; 94A3F7C8283734D900C48E79 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */; }; 94A3F7CA28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; }; 94A3F7CB28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; }; @@ -374,6 +375,7 @@ 9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */, 94CD196B283E666900973B93 /* EmojiManager.swift in Sources */, 94867144283ECD370093C7A4 /* Actions.swift in Sources */, + 94867145284058C60093C7A4 /* ApiService.swift in Sources */, 9474F2052831D51500CDE4DD /* Store.swift in Sources */, 9474F2062831D73C00CDE4DD /* ntfy.xcdatamodeld in Sources */, 94A3F7CB28386B2100C48E79 /* Config.swift in Sources */, diff --git a/ntfy/Persistence/Notification.swift b/ntfy/Persistence/Notification.swift index b372f5c..38d0067 100644 --- a/ntfy/Persistence/Notification.swift +++ b/ntfy/Persistence/Notification.swift @@ -71,6 +71,7 @@ struct Message: Decodable { var tags: [String]? var actions: [Action]? var click: String? + var pollId: String? func toUserInfo() -> [AnyHashable: Any] { // This should mimic the way that the ntfy server encodes a message. @@ -85,7 +86,8 @@ struct Message: Decodable { "priority": String(priority ?? 3), "tags": tags?.joined(separator: ",") ?? "", "actions": Actions.shared.encode(actions), - "click": click ?? "" + "click": click ?? "", + "poll_id": pollId ?? "" ] } @@ -103,6 +105,7 @@ struct Message: Decodable { let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",") let actions = userInfo["actions"] as? String let click = userInfo["click"] as? String + let pollId = userInfo["poll_id"] as? String return Message( id: id, time: timeInt, @@ -112,7 +115,8 @@ struct Message: Decodable { priority: priority, tags: tags, actions: Actions.shared.parse(actions), - click: click + click: click, + pollId: pollId ) } } diff --git a/ntfy/Persistence/Subscription.swift b/ntfy/Persistence/Subscription.swift index 819ca7d..317ff6e 100644 --- a/ntfy/Persistence/Subscription.swift +++ b/ntfy/Persistence/Subscription.swift @@ -10,7 +10,11 @@ extension Subscription { } func topicName() -> String { - return topic ?? "" + return topic ?? "?" + } + + func urlHash() -> String { + return topicHash(baseUrl: baseUrl ?? "?", topic: topic ?? "?") } func notificationCount() -> Int { diff --git a/ntfy/Persistence/SubscriptionManager.swift b/ntfy/Persistence/SubscriptionManager.swift index f621cb9..ba0ce60 100644 --- a/ntfy/Persistence/SubscriptionManager.swift +++ b/ntfy/Persistence/SubscriptionManager.swift @@ -9,7 +9,11 @@ struct SubscriptionManager { func subscribe(baseUrl: String, topic: String) { Log.d(tag, "Subscribing to \(topicUrl(baseUrl: baseUrl, topic: topic))") - Messaging.messaging().subscribe(toTopic: topic) + if baseUrl == Config.appBaseUrl { + Messaging.messaging().subscribe(toTopic: topic) + } else { + Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic)) + } let subscription = store.saveSubscription(baseUrl: baseUrl, topic: topic) poll(subscription) } @@ -17,8 +21,12 @@ struct SubscriptionManager { func unsubscribe(_ subscription: Subscription) { Log.d(tag, "Unsubscribing from \(subscription.urlString())") DispatchQueue.main.async { - if let topic = subscription.topic { - Messaging.messaging().unsubscribe(fromTopic: topic) + if let baseUrl = subscription.baseUrl, let topic = subscription.topic { + if baseUrl == Config.appBaseUrl { + Messaging.messaging().unsubscribe(fromTopic: topic) + } else { + Messaging.messaging().unsubscribe(fromTopic: topicHash(baseUrl: baseUrl, topic: topic)) + } } store.delete(subscription: subscription) } diff --git a/ntfy/Utils/ApiService.swift b/ntfy/Utils/ApiService.swift index cde771b..469c591 100644 --- a/ntfy/Utils/ApiService.swift +++ b/ntfy/Utils/ApiService.swift @@ -5,13 +5,34 @@ class ApiService { static let shared = ApiService() func poll(subscription: Subscription, completionHandler: @escaping ([Message]?, Error?) -> Void) { - guard let url = URL(string: subscription.urlString()) else { return } + guard let url = URL(string: subscription.urlString()) else { + // FIXME + return + } let since = subscription.lastNotificationId ?? "all" let urlString = "\(url)/json?poll=1&since=\(since)" Log.d(tag, "Polling from \(urlString)") fetchJsonData(urlString: urlString, completionHandler: completionHandler) } + + func poll(subscription: Subscription, messageId: String, completionHandler: @escaping (Message?, Error?) -> Void) { + let url = URL(string: "\(subscription.urlString())/json?poll=1&id=\(messageId)")! + Log.d(tag, "Polling single message from \(url)") + + URLSession.shared.dataTask(with: URLRequest(url: url)) { (data, response, error) in + if let error = error { + completionHandler(nil, error) + return + } + do { + let message = try JSONDecoder().decode(Message.self, from: data!) + completionHandler(message, nil) + } catch { + completionHandler(nil, error) + } + }.resume() + } func publish( subscription: Subscription, diff --git a/ntfy/Utils/Helpers.swift b/ntfy/Utils/Helpers.swift index 46f5315..0bd1820 100644 --- a/ntfy/Utils/Helpers.swift +++ b/ntfy/Utils/Helpers.swift @@ -1,4 +1,5 @@ import Foundation +import CryptoKit func topicUrl(baseUrl: String, topic: String) -> String { return "\(baseUrl)/\(topic)" @@ -10,6 +11,12 @@ func topicShortUrl(baseUrl: String, topic: String) -> String { .replacingOccurrences(of: "https://", with: "") } +func topicHash(baseUrl: String, topic: String) -> String { + let data = Data(topicUrl(baseUrl: baseUrl, topic: topic).utf8) + let digest = SHA256.hash(data: data) + return digest.compactMap { String(format: "%02x", $0)}.joined() +} + func parseAllTags(_ tags: String?) -> [String] { return (tags?.components(separatedBy: ",") ?? []) .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } diff --git a/ntfyNSE/NotificationService.swift b/ntfyNSE/NotificationService.swift index f99752a..eb7110f 100644 --- a/ntfyNSE/NotificationService.swift +++ b/ntfyNSE/NotificationService.swift @@ -1,5 +1,6 @@ import UserNotifications import CoreData +import CryptoKit /// This app extension is responsible for persisting the incoming notification to the data store (Core Data). It will eventually be the entity that /// fetches notification content from selfhosted servers (when a "poll request" is received). This is not implemented yet. @@ -22,11 +23,37 @@ class NotificationService: UNNotificationServiceExtension { if let bestAttemptContent = bestAttemptContent { let store = Store.shared let userInfo = bestAttemptContent.userInfo + let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl + let topic = userInfo["topic"] 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 == "poll_request" { + let subscription = store.getSubscriptions()?.first { $0.urlHash() == topic } + guard let subscription = subscription, let pollId = message.pollId else { + Log.w(tag, "Cannot find subscription", message) + contentHandler(request.content) + return + } + //let semaphore = DispatchSemaphore(value: 0) + ApiService.shared.poll(subscription: subscription, messageId: pollId) { message, error in + guard let message = message else { + Log.w(self.tag, "Error fetching message", error) + contentHandler(request.content) + return + } + bestAttemptContent.title = message.title ?? subscription.urlString() + bestAttemptContent.body = message.message ?? "" + contentHandler(bestAttemptContent) + //semaphore.signal() + } + //semaphore.wait(timeout: .distantFuture) + Thread.sleep(forTimeInterval: 5) + return + } + if message.event != "message" { Log.w(tag, "Irrelevant message received", message) contentHandler(request.content) @@ -34,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension { } // Only handle "message" events - 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) @@ -115,4 +140,12 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } + + func handleMessage() { + + } + + func handlePollRequest() { + + } }