From 9ec3c223174b07a19d08e6af74d1186d2de03704 Mon Sep 17 00:00:00 2001 From: Alek Michelson Date: Wed, 8 Apr 2026 18:58:40 -0400 Subject: [PATCH] fix iOS topic normalization and refresh after test push --- ntfy/App/AppDelegate.swift | 9 ++++---- ntfy/Persistence/Store.swift | 19 +++++++++------ ntfy/Persistence/SubscriptionManager.swift | 27 ++++++++++++---------- ntfy/Utils/ApiService.swift | 4 +++- ntfy/Utils/Helpers.swift | 20 +++++++++++++--- ntfy/Views/NotificationListView.swift | 5 +++- ntfy/Views/SettingsView.swift | 2 +- ntfy/Views/SubscriptionAddView.swift | 8 +++---- 8 files changed, 61 insertions(+), 33 deletions(-) diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 2862acf..25b2781 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -142,14 +142,15 @@ extension AppDelegate: MessagingDelegate { // Re-subscribe to Firebase for all topics let store = Store.shared + let subscriptionManager = SubscriptionManager(store: store) store.getSubscriptions()?.forEach{ subscription in if let baseUrl = subscription.baseUrl, let topic = subscription.topic { Log.d(tag, "Re-subscribing to topic \(baseUrl)/\(topic)") - if baseUrl == Config.appBaseUrl { - Messaging.messaging().subscribe(toTopic: topic) - } else { - Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic)) + if normalizeBaseUrl(baseUrl) == "https://ntfy.sh" && normalizeBaseUrl(Config.appBaseUrl) != "https://ntfy.sh" { + subscriptionManager.rebase(subscription, to: Config.appBaseUrl) + return } + Messaging.messaging().subscribe(toTopic: firebaseTopic(baseUrl: baseUrl, topic: topic)) } } } diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index c01fe8e..e5bc653 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -77,10 +77,10 @@ class Store: ObservableObject { func saveSubscription(baseUrl: String, topic: String) -> Subscription { let subscription = Subscription(context: context) - subscription.baseUrl = baseUrl + subscription.baseUrl = normalizeBaseUrl(baseUrl) subscription.topic = topic DispatchQueue.main.sync { - Log.d(Store.tag, "Storing subscription baseUrl=\(baseUrl), topic=\(topic)") + Log.d(Store.tag, "Storing subscription baseUrl=\(subscription.baseUrl ?? "?"), topic=\(topic)") try? context.save() } return subscription @@ -88,7 +88,7 @@ class Store: ObservableObject { func getSubscription(baseUrl: String, topic: String) -> Subscription? { let fetchRequest = Subscription.fetchRequest() - let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", baseUrl) + let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", normalizeBaseUrl(baseUrl)) let topicPredicate = NSPredicate(format: "topic = %@", topic) fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [baseUrlPredicate, topicPredicate]) @@ -99,6 +99,11 @@ class Store: ObservableObject { func getSubscriptions() -> [Subscription]? { return try? context.fetch(Subscription.fetchRequest()) } + + func updateSubscriptionBaseUrl(_ subscription: Subscription, baseUrl: String) { + subscription.baseUrl = normalizeBaseUrl(baseUrl) + try? context.save() + } func delete(subscription: Subscription) { context.delete(subscription) @@ -167,7 +172,7 @@ class Store: ObservableObject { func saveUser(baseUrl: String, username: String, password: String) { do { let user = getUser(baseUrl: baseUrl) ?? User(context: context) - user.baseUrl = baseUrl + user.baseUrl = normalizeBaseUrl(baseUrl) user.username = username user.password = password try context.save() @@ -179,7 +184,7 @@ class Store: ObservableObject { func getUser(baseUrl: String) -> User? { let request = User.fetchRequest() - request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "baseUrl = %@", baseUrl)]) + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "baseUrl = %@", normalizeBaseUrl(baseUrl))]) return try? context.fetch(request).first } @@ -194,7 +199,7 @@ class Store: ObservableObject { do { let pref = getPreference(key: Store.prefKeyDefaultBaseUrl) ?? Preference(context: context) pref.key = Store.prefKeyDefaultBaseUrl - pref.value = baseUrl ?? Config.appBaseUrl + pref.value = baseUrl.map(normalizeBaseUrl) ?? Config.appBaseUrl try context.save() } catch let error { Log.w(Store.tag, "Cannot store preference", error) @@ -207,7 +212,7 @@ class Store: ObservableObject { if baseUrl == nil || baseUrl?.isEmpty == true { return Config.appBaseUrl } - return baseUrl! + return normalizeBaseUrl(baseUrl!) } private func getPreference(key: String) -> Preference? { diff --git a/ntfy/Persistence/SubscriptionManager.swift b/ntfy/Persistence/SubscriptionManager.swift index 8526a84..73814bc 100644 --- a/ntfy/Persistence/SubscriptionManager.swift +++ b/ntfy/Persistence/SubscriptionManager.swift @@ -8,13 +8,10 @@ struct SubscriptionManager { var store: Store func subscribe(baseUrl: String, topic: String) { - Log.d(tag, "Subscribing to \(topicUrl(baseUrl: baseUrl, topic: 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) + let normalizedBaseUrl = normalizeBaseUrl(baseUrl) + Log.d(tag, "Subscribing to \(topicUrl(baseUrl: normalizedBaseUrl, topic: topic))") + Messaging.messaging().subscribe(toTopic: firebaseTopic(baseUrl: normalizedBaseUrl, topic: topic)) + let subscription = store.saveSubscription(baseUrl: normalizedBaseUrl, topic: topic) poll(subscription) } @@ -22,15 +19,21 @@ struct SubscriptionManager { Log.d(tag, "Unsubscribing from \(subscription.urlString())") DispatchQueue.main.async { 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)) - } + Messaging.messaging().unsubscribe(fromTopic: firebaseTopic(baseUrl: baseUrl, topic: topic)) } store.delete(subscription: subscription) } } + + func rebase(_ subscription: Subscription, to baseUrl: String) { + guard let oldBaseUrl = subscription.baseUrl, let topic = subscription.topic else { return } + let normalizedBaseUrl = normalizeBaseUrl(baseUrl) + if normalizeBaseUrl(oldBaseUrl) == normalizedBaseUrl { return } + Log.d(tag, "Updating subscription \(topic) from \(oldBaseUrl) to \(normalizedBaseUrl)") + Messaging.messaging().unsubscribe(fromTopic: firebaseTopic(baseUrl: oldBaseUrl, topic: topic)) + Messaging.messaging().subscribe(toTopic: firebaseTopic(baseUrl: normalizedBaseUrl, topic: topic)) + store.updateSubscriptionBaseUrl(subscription, baseUrl: normalizedBaseUrl) + } func poll(_ subscription: Subscription) { poll(subscription) { _ in } diff --git a/ntfy/Utils/ApiService.swift b/ntfy/Utils/ApiService.swift index 1a1a629..97aafca 100644 --- a/ntfy/Utils/ApiService.swift +++ b/ntfy/Utils/ApiService.swift @@ -43,7 +43,8 @@ class ApiService { message: String, title: String, priority: Int = 3, - tags: [String] = [] + tags: [String] = [], + completionHandler: (() -> Void)? = nil ) { guard let url = URL(string: subscription.urlString()) else { return } var request = newRequest(url: url, user: user) @@ -61,6 +62,7 @@ class ApiService { return } Log.d(self.tag, "Publishing message succeeded", response) + completionHandler?() }.resume() } diff --git a/ntfy/Utils/Helpers.swift b/ntfy/Utils/Helpers.swift index 18cb11f..764000e 100644 --- a/ntfy/Utils/Helpers.swift +++ b/ntfy/Utils/Helpers.swift @@ -2,7 +2,7 @@ import Foundation import CryptoKit func topicUrl(baseUrl: String, topic: String) -> String { - return "\(baseUrl)/\(topic)" + return "\(normalizeBaseUrl(baseUrl))/\(topic)" } func topicShortUrl(baseUrl: String, topic: String) -> String { @@ -10,15 +10,29 @@ func topicShortUrl(baseUrl: String, topic: String) -> String { } func topicAuthUrl(baseUrl: String, topic: String) -> String { - return "\(baseUrl)/\(topic)/auth" + return "\(normalizeBaseUrl(baseUrl))/\(topic)/auth" } func topicHash(baseUrl: String, topic: String) -> String { - let data = Data(topicUrl(baseUrl: baseUrl, topic: topic).utf8) + let data = Data(topicUrl(baseUrl: normalizeBaseUrl(baseUrl), topic: topic).utf8) let digest = SHA256.hash(data: data) return digest.compactMap { String(format: "%02x", $0)}.joined() } +func firebaseTopic(baseUrl: String, topic: String) -> String { + return normalizeBaseUrl(baseUrl) == normalizeBaseUrl(Config.appBaseUrl) + ? topic + : topicHash(baseUrl: baseUrl, topic: topic) +} + +func normalizeBaseUrl(_ baseUrl: String) -> String { + var normalized = baseUrl.trimmingCharacters(in: .whitespacesAndNewlines) + while normalized.hasSuffix("/") { + normalized.removeLast() + } + return normalized +} + func shortUrl(url: String) -> String { return url .replacingOccurrences(of: "http://", with: "") diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index b4d94c3..cc3cb30 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -5,6 +5,7 @@ enum ActiveAlert { case clear, unsubscribe, selected } + struct NotificationListView: View { private let tag = "NotificationListView" @@ -201,7 +202,9 @@ struct NotificationListView: View { title: "Test: You can set a title if you like", priority: priority, tags: tags - ) + ) { + subscriptionManager.poll(subscription) + } } } diff --git a/ntfy/Views/SettingsView.swift b/ntfy/Views/SettingsView.swift index 856f6fb..638edd3 100644 --- a/ntfy/Views/SettingsView.swift +++ b/ntfy/Views/SettingsView.swift @@ -106,7 +106,7 @@ struct DefaultServerView: View { if newDefaultBaseUrl == "" { store.saveDefaultBaseUrl(baseUrl: nil) } else { - store.saveDefaultBaseUrl(baseUrl: newDefaultBaseUrl) + store.saveDefaultBaseUrl(baseUrl: normalizeBaseUrl(newDefaultBaseUrl)) } resetAndHide() } diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index 2fc8645..e0337b0 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -136,7 +136,7 @@ struct SubscriptionAddView: View { return false } else if selectedBaseUrl.range(of: "^https?://.+", options: .regularExpression, range: nil, locale: nil) == nil { return false - } else if store.getSubscription(baseUrl: selectedBaseUrl, topic: topic) != nil { + } else if store.getSubscription(baseUrl: selectedBaseUrl, topic: sanitizedTopic) != nil { return false } return true @@ -153,7 +153,7 @@ struct SubscriptionAddView: View { loading = true addError = nil let user = store.getUser(baseUrl: selectedBaseUrl)?.toBasicUser() - ApiService.shared.checkAuth(baseUrl: selectedBaseUrl, topic: topic, user: user) { result in + ApiService.shared.checkAuth(baseUrl: selectedBaseUrl, topic: sanitizedTopic, user: user) { result in switch result { case .Success: DispatchQueue.global(qos: .background).async { @@ -180,7 +180,7 @@ struct SubscriptionAddView: View { loading = true loginError = nil let user = BasicUser(username: username, password: password) - ApiService.shared.checkAuth(baseUrl: selectedBaseUrl, topic: topic, user: user) { result in + ApiService.shared.checkAuth(baseUrl: selectedBaseUrl, topic: sanitizedTopic, user: user) { result in switch result { case .Success: DispatchQueue.global(qos: .background).async { @@ -204,7 +204,7 @@ struct SubscriptionAddView: View { } private var selectedBaseUrl: String { - return (useAnother) ? baseUrl : store.getDefaultBaseUrl() + return normalizeBaseUrl((useAnother) ? baseUrl : store.getDefaultBaseUrl()) } private func resetAndHide() {