Merge pull request #32 from am7590/stability-fixes

fix iOS topic normalization and refresh after test push
This commit is contained in:
Philipp C. Heckel 2026-04-09 18:46:17 -04:00 committed by GitHub
commit 06e90baf09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 48 additions and 34 deletions

View file

@ -142,14 +142,11 @@ extension AppDelegate: MessagingDelegate {
// Re-subscribe to Firebase for all topics // Re-subscribe to Firebase for all topics
let store = Store.shared let store = Store.shared
let subscriptionManager = SubscriptionManager(store: store)
store.getSubscriptions()?.forEach{ subscription in store.getSubscriptions()?.forEach{ subscription in
if let baseUrl = subscription.baseUrl, let topic = subscription.topic { if let baseUrl = subscription.baseUrl, let topic = subscription.topic {
Log.d(tag, "Re-subscribing to topic \(baseUrl)/\(topic)") Log.d(tag, "Re-subscribing to topic \(baseUrl)/\(topic)")
if baseUrl == Config.appBaseUrl { Messaging.messaging().subscribe(toTopic: firebaseTopic(baseUrl: baseUrl, topic: topic))
Messaging.messaging().subscribe(toTopic: topic)
} else {
Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic))
}
} }
} }
} }

View file

@ -77,10 +77,10 @@ class Store: ObservableObject {
func saveSubscription(baseUrl: String, topic: String) -> Subscription { func saveSubscription(baseUrl: String, topic: String) -> Subscription {
let subscription = Subscription(context: context) let subscription = Subscription(context: context)
subscription.baseUrl = baseUrl subscription.baseUrl = normalizeBaseUrl(baseUrl)
subscription.topic = topic subscription.topic = topic
DispatchQueue.main.sync { 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() try? context.save()
} }
return subscription return subscription
@ -88,7 +88,7 @@ class Store: ObservableObject {
func getSubscription(baseUrl: String, topic: String) -> Subscription? { func getSubscription(baseUrl: String, topic: String) -> Subscription? {
let fetchRequest = Subscription.fetchRequest() let fetchRequest = Subscription.fetchRequest()
let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", baseUrl) let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", normalizeBaseUrl(baseUrl))
let topicPredicate = NSPredicate(format: "topic = %@", topic) let topicPredicate = NSPredicate(format: "topic = %@", topic)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [baseUrlPredicate, topicPredicate]) fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [baseUrlPredicate, topicPredicate])
@ -99,6 +99,11 @@ class Store: ObservableObject {
func getSubscriptions() -> [Subscription]? { func getSubscriptions() -> [Subscription]? {
return try? context.fetch(Subscription.fetchRequest()) return try? context.fetch(Subscription.fetchRequest())
} }
func updateSubscriptionBaseUrl(_ subscription: Subscription, baseUrl: String) {
subscription.baseUrl = normalizeBaseUrl(baseUrl)
try? context.save()
}
func delete(subscription: Subscription) { func delete(subscription: Subscription) {
context.performAndWait { context.performAndWait {
@ -175,7 +180,7 @@ class Store: ObservableObject {
func saveUser(baseUrl: String, username: String, password: String) { func saveUser(baseUrl: String, username: String, password: String) {
do { do {
let user = getUser(baseUrl: baseUrl) ?? User(context: context) let user = getUser(baseUrl: baseUrl) ?? User(context: context)
user.baseUrl = baseUrl user.baseUrl = normalizeBaseUrl(baseUrl)
user.username = username user.username = username
user.password = password user.password = password
try context.save() try context.save()
@ -187,7 +192,7 @@ class Store: ObservableObject {
func getUser(baseUrl: String) -> User? { func getUser(baseUrl: String) -> User? {
let request = User.fetchRequest() 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 return try? context.fetch(request).first
} }
@ -202,7 +207,7 @@ class Store: ObservableObject {
do { do {
let pref = getPreference(key: Store.prefKeyDefaultBaseUrl) ?? Preference(context: context) let pref = getPreference(key: Store.prefKeyDefaultBaseUrl) ?? Preference(context: context)
pref.key = Store.prefKeyDefaultBaseUrl pref.key = Store.prefKeyDefaultBaseUrl
pref.value = baseUrl ?? Config.appBaseUrl pref.value = baseUrl.map(normalizeBaseUrl) ?? Config.appBaseUrl
try context.save() try context.save()
} catch let error { } catch let error {
Log.w(Store.tag, "Cannot store preference", error) Log.w(Store.tag, "Cannot store preference", error)
@ -215,7 +220,7 @@ class Store: ObservableObject {
if baseUrl == nil || baseUrl?.isEmpty == true { if baseUrl == nil || baseUrl?.isEmpty == true {
return Config.appBaseUrl return Config.appBaseUrl
} }
return baseUrl! return normalizeBaseUrl(baseUrl!)
} }
private func getPreference(key: String) -> Preference? { private func getPreference(key: String) -> Preference? {

View file

@ -8,13 +8,10 @@ struct SubscriptionManager {
var store: Store var store: Store
func subscribe(baseUrl: String, topic: String) { func subscribe(baseUrl: String, topic: String) {
Log.d(tag, "Subscribing to \(topicUrl(baseUrl: baseUrl, topic: topic))") let normalizedBaseUrl = normalizeBaseUrl(baseUrl)
if baseUrl == Config.appBaseUrl { Log.d(tag, "Subscribing to \(topicUrl(baseUrl: normalizedBaseUrl, topic: topic))")
Messaging.messaging().subscribe(toTopic: topic) Messaging.messaging().subscribe(toTopic: firebaseTopic(baseUrl: normalizedBaseUrl, topic: topic))
} else { let subscription = store.saveSubscription(baseUrl: normalizedBaseUrl, topic: topic)
Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic))
}
let subscription = store.saveSubscription(baseUrl: baseUrl, topic: topic)
poll(subscription) poll(subscription)
} }
@ -22,11 +19,7 @@ struct SubscriptionManager {
Log.d(tag, "Unsubscribing from \(subscription.urlString())") Log.d(tag, "Unsubscribing from \(subscription.urlString())")
DispatchQueue.main.async { DispatchQueue.main.async {
if let baseUrl = subscription.baseUrl, let topic = subscription.topic { if let baseUrl = subscription.baseUrl, let topic = subscription.topic {
if baseUrl == Config.appBaseUrl { Messaging.messaging().unsubscribe(fromTopic: firebaseTopic(baseUrl: baseUrl, topic: topic))
Messaging.messaging().unsubscribe(fromTopic: topic)
} else {
Messaging.messaging().unsubscribe(fromTopic: topicHash(baseUrl: baseUrl, topic: topic))
}
} }
store.delete(subscription: subscription) store.delete(subscription: subscription)
} }

View file

@ -43,7 +43,8 @@ class ApiService {
message: String, message: String,
title: String, title: String,
priority: Int = 3, priority: Int = 3,
tags: [String] = [] tags: [String] = [],
completionHandler: (() -> Void)? = nil
) { ) {
guard let url = URL(string: subscription.urlString()) else { return } guard let url = URL(string: subscription.urlString()) else { return }
var request = newRequest(url: url, user: user) var request = newRequest(url: url, user: user)
@ -61,6 +62,7 @@ class ApiService {
return return
} }
Log.d(self.tag, "Publishing message succeeded", response) Log.d(self.tag, "Publishing message succeeded", response)
completionHandler?()
}.resume() }.resume()
} }

View file

@ -2,7 +2,7 @@ import Foundation
import CryptoKit import CryptoKit
func topicUrl(baseUrl: String, topic: String) -> String { func topicUrl(baseUrl: String, topic: String) -> String {
return "\(baseUrl)/\(topic)" return "\(normalizeBaseUrl(baseUrl))/\(topic)"
} }
func topicShortUrl(baseUrl: String, topic: String) -> String { 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 { func topicAuthUrl(baseUrl: String, topic: String) -> String {
return "\(baseUrl)/\(topic)/auth" return "\(normalizeBaseUrl(baseUrl))/\(topic)/auth"
} }
func topicHash(baseUrl: String, topic: String) -> String { 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) let digest = SHA256.hash(data: data)
return digest.compactMap { String(format: "%02x", $0)}.joined() 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 { func shortUrl(url: String) -> String {
return url return url
.replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "http://", with: "")

View file

@ -5,6 +5,7 @@ enum ActiveAlert {
case clear, unsubscribe, selected case clear, unsubscribe, selected
} }
struct NotificationListView: View { struct NotificationListView: View {
private let tag = "NotificationListView" private let tag = "NotificationListView"
@ -201,7 +202,9 @@ struct NotificationListView: View {
title: "Test: You can set a title if you like", title: "Test: You can set a title if you like",
priority: priority, priority: priority,
tags: tags tags: tags
) ) {
subscriptionManager.poll(subscription)
}
} }
} }

View file

@ -106,7 +106,7 @@ struct DefaultServerView: View {
if newDefaultBaseUrl == "" { if newDefaultBaseUrl == "" {
store.saveDefaultBaseUrl(baseUrl: nil) store.saveDefaultBaseUrl(baseUrl: nil)
} else { } else {
store.saveDefaultBaseUrl(baseUrl: newDefaultBaseUrl) store.saveDefaultBaseUrl(baseUrl: normalizeBaseUrl(newDefaultBaseUrl))
} }
resetAndHide() resetAndHide()
} }

View file

@ -136,7 +136,7 @@ struct SubscriptionAddView: View {
return false return false
} else if selectedBaseUrl.range(of: "^https?://.+", options: .regularExpression, range: nil, locale: nil) == nil { } else if selectedBaseUrl.range(of: "^https?://.+", options: .regularExpression, range: nil, locale: nil) == nil {
return false return false
} else if store.getSubscription(baseUrl: selectedBaseUrl, topic: topic) != nil { } else if store.getSubscription(baseUrl: selectedBaseUrl, topic: sanitizedTopic) != nil {
return false return false
} }
return true return true
@ -153,7 +153,7 @@ struct SubscriptionAddView: View {
loading = true loading = true
addError = nil addError = nil
let user = store.getUser(baseUrl: selectedBaseUrl)?.toBasicUser() 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 { switch result {
case .Success: case .Success:
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
@ -180,7 +180,7 @@ struct SubscriptionAddView: View {
loading = true loading = true
loginError = nil loginError = nil
let user = BasicUser(username: username, password: password) 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 { switch result {
case .Success: case .Success:
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
@ -204,7 +204,7 @@ struct SubscriptionAddView: View {
} }
private var selectedBaseUrl: String { private var selectedBaseUrl: String {
return (useAnother) ? baseUrl : store.getDefaultBaseUrl() return normalizeBaseUrl((useAnother) ? baseUrl : store.getDefaultBaseUrl())
} }
private func resetAndHide() { private func resetAndHide() {