diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 88d29e2..60c562c 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 9474F2132834755A00CDE4DD /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20B283321C300CDE4DD /* Notification.swift */; }; 9474F2142834755E00CDE4DD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1FE28316ACE00CDE4DD /* Subscription.swift */; }; 9474F2152834758700CDE4DD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F211283327C200CDE4DD /* Helpers.swift */; }; + 9474F217283531A300CDE4DD /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; + 94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -83,6 +85,7 @@ 9474F20B283321C300CDE4DD /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 9474F20E283326C500CDE4DD /* ApiService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; 9474F211283327C200CDE4DD /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + 9474F216283531A200CDE4DD /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -202,6 +205,7 @@ children = ( 9474F20E283326C500CDE4DD /* ApiService.swift */, 9474F211283327C200CDE4DD /* Helpers.swift */, + 9474F216283531A200CDE4DD /* Log.swift */, ); path = Utils; sourceTree = ""; @@ -315,6 +319,7 @@ files = ( 9474F1F92830835400CDE4DD /* Store.swift in Sources */, 9474F212283327C200CDE4DD /* Helpers.swift in Sources */, + 9474F217283531A300CDE4DD /* Log.swift in Sources */, 9474F20928331F3A00CDE4DD /* NotificationListView.swift in Sources */, 9474F20A28331F3A00CDE4DD /* NotificationRowView.swift in Sources */, 9474F1D2282F2D2C00CDE4DD /* AppDelegate.swift in Sources */, @@ -335,6 +340,7 @@ buildActionMask = 2147483647; files = ( 9474F2132834755A00CDE4DD /* Notification.swift in Sources */, + 94E9196C28353E0100F30170 /* Log.swift in Sources */, 9474F2152834758700CDE4DD /* Helpers.swift in Sources */, 9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */, 9474F2052831D51500CDE4DD /* Store.swift in Sources */, diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 9fac1ce..5e0dfa9 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -9,14 +9,14 @@ import CoreData // https://stackoverflow.com/questions/47374903/viewing-core-data-data-from-your-app-on-a-device class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? + let tag = "AppDelegate" let store = Store.shared func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - print("ApplicationDelegate didFinishLaunchingWithOptions.") + Log.d(tag, "ApplicationDelegate didFinishLaunchingWithOptions.") // FirebaseApp.configure() DOES NOT WORK FirebaseConfiguration.shared.setLoggerLevel(.max) Messaging.messaging().delegate = self @@ -24,23 +24,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { registerForPushNotifications() UNUserNotificationCenter.current().delegate = self - - - print("Documents Directory: ", FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last ?? "Not Found!") - - // Check if launched from notification - let notificationOption = launchOptions?[.remoteNotification] - - // 1 - if - let notification = notificationOption as? [String: AnyObject], - let aps = notification["aps"] as? [String: AnyObject] { - print("there is a new item") - // 2 - - // 3 - (window?.rootViewController as? UITabBarController)?.selectedIndex = 1 - } return true } @@ -108,33 +91,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - - lazy var persistentContainer: NSPersistentContainer = { - let container = NSPersistentContainer(name: "Model") - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - return container - }() - - // MARK: - Core Data Saving support - - func saveContext () { - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } - } - } - - } extension AppDelegate: UNUserNotificationCenterDelegate { @@ -144,8 +100,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { let userInfo = notification.request.content.userInfo - print("willPresent", userInfo) - + Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo) store.saveNotification(fromUserInfo: userInfo) completionHandler([[.alert, .sound]]) } @@ -155,18 +110,21 @@ extension AppDelegate: UNUserNotificationCenterDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { - print("didReceive") let userInfo = response.notification.request.content.userInfo - + Log.d(tag, "Notification received via userNotificationCenter(didReceive)", userInfo) + store.saveNotification(fromUserInfo: userInfo) completionHandler() } } -// [END ios_10_message_handling] extension AppDelegate: MessagingDelegate { - // [START refresh_token] - func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - print("Firebase registration token: \(String(describing: fcmToken))") + func messaging( + _ messaging: Messaging, + didReceiveRegistrationToken fcmToken: String? + ) { + Log.d(tag, "Firebase token received: \(String(describing: fcmToken))") + + // FIXME: Is this necessary? let dataDict: [String: String] = ["token": fcmToken ?? ""] NotificationCenter.default.post( @@ -174,8 +132,6 @@ extension AppDelegate: MessagingDelegate { object: nil, userInfo: dataDict ) - // TODO: If necessary send token to application server. - // Note: This callback is fired at each app startup and whenever a new token is generated. } } diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 8564ec6..0b5e8e0 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -11,11 +11,17 @@ import CoreData class Store: ObservableObject { static let shared = Store() + let tag = "Store" let container: NSPersistentContainer var context: NSManagedObjectContext init() { + let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.io.heckel.ntfy")! + let storeUrl = directory.appendingPathComponent("ntfy.sqlite") + let description = NSPersistentStoreDescription(url: storeUrl) + container = NSPersistentContainer(name: "Model") + container.persistentStoreDescriptions = [description] container.loadPersistentStores { description, error in if let error = error { print("Core Data failed to load: \(error.localizedDescription)") @@ -31,7 +37,6 @@ class Store: ObservableObject { try? context.save() } - func getSubscription(baseUrl: String, topic: String) -> Subscription? { let fetchRequest = Subscription.fetchRequest() let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", baseUrl) @@ -44,7 +49,7 @@ class Store: ObservableObject { func saveNotification(fromUserInfo userInfo: [AnyHashable: Any]) { guard let id = userInfo["id"] as? String, - let topic = userInfo["topic"] as? String, + let topic = userInfo["topic"] as? String, // FIXME: Notification should also contain baseUrl let time = userInfo["time"] as? String, let timeInt = Int64(time), let message = userInfo["message"] as? String else { @@ -65,7 +70,7 @@ class Store: ObservableObject { subscription.addToNotifications(notification) try context.save() } catch let error { - print(error) + Log.w(tag, "Cannot store notification", error) context.rollback() } } diff --git a/ntfy/Utils/ApiService.swift b/ntfy/Utils/ApiService.swift index 085c576..912f160 100644 --- a/ntfy/Utils/ApiService.swift +++ b/ntfy/Utils/ApiService.swift @@ -1,26 +1,32 @@ -// -// ApiService.swift -// ntfy.sh -// -// Created by Andrew Cope on 2/16/22. -// - import Foundation class ApiService: NSObject { static let shared = ApiService() - + let tag = "ApiService" + func poll(subscription: Subscription, completionHandler: @escaping ([Message]?, Error?) -> Void) { - let lastNotificationTime = 0 //subscription.lastNotification()?.timestamp ?? 0 // FIXME + guard let url = URL(string: subscription.urlString()) else { return } + let lastNotificationTime = subscription.lastNotification()?.time ?? 0 let sinceString = lastNotificationTime > 0 ? String(lastNotificationTime) : "all"; - let urlString = "\(subscription.urlString())/json?poll=1&since=\(sinceString)" + let urlString = "\(url)/json?poll=1&since=\(sinceString)" + + Log.d(tag, "Polling from \(urlString)") fetchJsonData(urlString: urlString, completionHandler: completionHandler) } - func publish(subscription: Subscription, message: String, title: String, priority: Int = 3, tags: [String] = [], completionHandler: @escaping (Notification?, Error?) -> Void) { + func publish( + subscription: Subscription, + message: String, + title: String, + priority: Int = 3, + tags: [String] = [], + completionHandler: @escaping (Notification?, Error?) -> Void + ) { guard let url = URL(string: subscription.urlString()) else { return } var request = URLRequest(url: url) + Log.d(tag, "Publishing to \(url)") + request.httpMethod = "POST" request.setValue(title, forHTTPHeaderField: "Title") request.setValue(String(priority), forHTTPHeaderField: "Priority") @@ -35,7 +41,7 @@ class ApiService: NSObject { private func fetchJsonData(urlString: String, completionHandler: @escaping ([T]?, Error?) -> ()) { guard let url = URL(string: urlString) else { return } - var request = URLRequest(url: url) + let request = URLRequest(url: url) URLSession.shared.dataTask(with: request) { (data, response, error) in if let error = error { diff --git a/ntfy/Utils/Log.swift b/ntfy/Utils/Log.swift new file mode 100644 index 0000000..396a5fd --- /dev/null +++ b/ntfy/Utils/Log.swift @@ -0,0 +1,62 @@ +// +// Log.swift +// ntfy +// +// Created by Philipp Heckel on 5/18/22. +// + +import Foundation + +struct Log { + static var dateFormat = "yyyy-MM-dd hh:mm:ss.SSSSSSZ" + static var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = dateFormat + formatter.locale = Locale.current + formatter.timeZone = TimeZone.current + return formatter + } + + static func d(_ tag: String, _ message: String, _ other: Any...) { + log(.debug, tag, message, other) + } + + static func i(_ tag: String, _ message: String, _ other: Any...) { + log(.info, tag, message, other) + } + + static func w(_ tag: String, _ message: String, _ other: Any...) { + log(.warning, tag, message, other) + } + + static func e(_ tag: String, _ message: String, _ other: Any...) { + log(.error, tag, message, other) + } + + static func log(_ level: LogLevel, _ tag: String, _ message: String, _ other: Any...) { + print("\(dateStr()) ntfyApp [\(levelStr(level))] \(tag): \(message)") + if !other.isEmpty { + print(other) + } + } + + static func dateStr() -> String { + return dateFormatter.string(from: Date()) + } + + static func levelStr(_ level: LogLevel) -> String { + switch level { + case .debug: return "DEBUG" + case .info: return "INFO" + case .warning: return "WARNING ⚠️" + case .error: return "ERROR ‼️" + } + } +} + +enum LogLevel { + case debug + case info + case warning + case error +} diff --git a/ntfyNSE/NotificationService.swift b/ntfyNSE/NotificationService.swift index 42133cd..2b2366b 100644 --- a/ntfyNSE/NotificationService.swift +++ b/ntfyNSE/NotificationService.swift @@ -14,7 +14,10 @@ class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)