From 111e39b6eca24d75f853d5d005bbeb2402a97fa7 Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Sun, 21 Jul 2024 15:15:25 -0700 Subject: [PATCH] Adds opening 3rd part apps and triggering rules (#793) Signed-off-by: Dan Cunningham --- NotificationService/NotificationService.swift | 62 +++++++++--------- .../Sources/OpenHABCore/Util/HTTPClient.swift | 2 +- openHAB/OpenHABRootViewController.swift | 63 +++++++++++++++++++ openHABIntents/openHABIntents.entitlements | 4 ++ .../openHABWatch Extension.entitlements | 4 ++ 5 files changed, 101 insertions(+), 34 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index cf785e5a..038aacdc 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -18,23 +18,23 @@ import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent { var notificationActions: [UNNotificationAction] = [] let userInfo = bestAttemptContent.userInfo - + os_log("didReceive userInfo %{PUBLIC}@", log: .default, type: .info, userInfo) - + if let title = userInfo["title"] as? String { bestAttemptContent.title = title } if let message = userInfo["message"] as? String { bestAttemptContent.body = message } - + // Check if the user has defined custom actions in the payload if let actionsArray = parseActions(userInfo), let category = parseCategory(userInfo) { for actionDict in actionsArray { @@ -42,7 +42,7 @@ class NotificationService: UNNotificationServiceExtension { let title = actionDict["title"] { var options: UNNotificationActionOptions = [] // navigate/browser options need to bring the app to the foreground - if action.hasPrefix("ui") || action.hasPrefix("http") { + if action.hasPrefix("ui") || action.hasPrefix("http") || action.hasPrefix("app") { options = [.foreground] } let notificationAction = UNNotificationAction( @@ -56,31 +56,27 @@ class NotificationService: UNNotificationServiceExtension { if !notificationActions.isEmpty { os_log("didReceive registering %{PUBLIC}@ for category %{PUBLIC}@", log: .default, type: .info, notificationActions, category) let notificationCategory = - UNNotificationCategory( - identifier: category, - actions: notificationActions, - intentIdentifiers: [], - options: .customDismissAction - ) + UNNotificationCategory( + identifier: category, + actions: notificationActions, + intentIdentifiers: [], + options: .customDismissAction + ) UNUserNotificationCenter.current().getNotificationCategories { existingCategories in - // Check if the new category already exists, this is a hash of the actions string done by the cloud service - let existingCategoryIdentifiers = existingCategories.map(\.identifier) - if !existingCategoryIdentifiers.contains(category) { - var updatedCategories = existingCategories - os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category) - updatedCategories.insert(notificationCategory) - UNUserNotificationCenter.current().setNotificationCategories(updatedCategories) - } + var updatedCategories = existingCategories + os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category) + updatedCategories.insert(notificationCategory) + UNUserNotificationCenter.current().setNotificationCategories(updatedCategories) } } } - + // check if there is an attachment to put on the notification // this should be last as we need to wait for media // TODO: we should support relative paths and try the user's openHAB (local,remote) for content if let attachmentURLString = userInfo["media-attachment-url"] as? String { let isItem = attachmentURLString.starts(with: "item:") - + let downloadCompletionHandler: @Sendable (UNNotificationAttachment?) -> Void = { attachment in if let attachment { os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString) @@ -90,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension { } contentHandler(bestAttemptContent) } - + if isItem { downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler) } else { @@ -101,7 +97,7 @@ class NotificationService: UNNotificationServiceExtension { } } } - + override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. @@ -110,7 +106,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } - + private func parseActions(_ userInfo: [AnyHashable: Any]) -> [[String: String]]? { // Extract actions and convert it from JSON string to an array of dictionaries if let actionsString = userInfo["actions"] as? String, let actionsData = actionsString.data(using: .utf8) { @@ -124,7 +120,7 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func parseCategory(_ userInfo: [AnyHashable: Any]) -> String? { // Extract category from aps dictionary if let aps = userInfo["aps"] as? [String: Any], @@ -133,10 +129,10 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func downloadAndAttachMedia(url: String, completion: @escaping (UNNotificationAttachment?) -> Void) { let client = HTTPClient(username: Preferences.username, password: Preferences.username, alwaysSendBasicAuth: Preferences.alwaysSendCreds) - + let downloadCompletionHandler: @Sendable (URL?, URLResponse?, Error?) -> Void = { (localURL, response, error) in guard let localURL else { os_log("Error downloading media %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "Unknown error") @@ -151,16 +147,16 @@ class NotificationService: UNNotificationServiceExtension { client.downloadFile(url: url, completionHandler: downloadCompletionHandler) } } - + func downloadAndAttachItemImage(itemURI: String, completion: @escaping (UNNotificationAttachment?) -> Void) { guard let itemURI = URL(string: itemURI), let scheme = itemURI.scheme else { os_log("Could not find scheme %{PUBLIC}@", log: .default, type: .info) completion(nil) return } - + let itemName = String(itemURI.absoluteString.dropFirst(scheme.count + 1)) - + let client = HTTPClient(username: Preferences.username, password: Preferences.password, alwaysSendBasicAuth: Preferences.alwaysSendCreds) client.getItem(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], itemName: itemName) { item, error in guard let item else { @@ -203,16 +199,16 @@ class NotificationService: UNNotificationServiceExtension { completion(nil) } } - + func attachFile(localURL: URL, mimeType: String?, completion: @escaping (UNNotificationAttachment?) -> Void) { do { let fileManager = FileManager.default let tempDirectory = NSTemporaryDirectory() let tempFile = URL(fileURLWithPath: tempDirectory).appendingPathComponent(UUID().uuidString) - + try fileManager.moveItem(at: localURL, to: tempFile) let attachment: UNNotificationAttachment? - + if let mimeType, let utType = UTType(mimeType: mimeType), utType.conforms(to: .data) { diff --git a/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift b/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift index 425f62b4..9bd82403 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift @@ -282,7 +282,7 @@ public class HTTPClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate { } // MARK: - URLSessionDelegate for Client Certificates and Basic Auth - + public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { urlSessionInternal(session, task: nil, didReceive: challenge, completionHandler: completionHandler) } diff --git a/openHAB/OpenHABRootViewController.swift b/openHAB/OpenHABRootViewController.swift index f390dc6b..44ae076e 100644 --- a/openHAB/OpenHABRootViewController.swift +++ b/openHAB/OpenHABRootViewController.swift @@ -202,6 +202,10 @@ class OpenHABRootViewController: UIViewController { sendCommandAction(cmd) } else if action.hasPrefix("http") { httpCommandAction(action) + } else if action.hasPrefix("app") { + appCommandAction(action) + } else if action.hasPrefix("rule") { + ruleCommandAction(action) } } } @@ -265,6 +269,65 @@ class OpenHABRootViewController: UIViewController { } } + private func appCommandAction(_ command: String) { + let content = command.dropFirst(4) // Remove "app:" + let pairs = content.split(separator: ",") + for pair in pairs { + let keyValue = pair.split(separator: "=", maxSplits: 1) + guard keyValue.count == 2 else { continue } + if keyValue[0] == "ios" { + if let url = URL(string: String(keyValue[1])) { + os_log("appCommandAction opening %{public}@ %{public}@", log: .default, type: .error, String(keyValue[0]), String(keyValue[1])) + UIApplication.shared.open(url) + return + } + } + } + } + + private func ruleCommandAction(_ command: String) { + let components = command.split(separator: ":", maxSplits: 2) + + guard components.count == 3, + components[0] == "rule" else { + return + } + + let uuid = String(components[1]) + let propertiesString = String(components[2]) + + let propertyPairs = propertiesString.split(separator: ",") + var properties: [String: String] = [:] + + for pair in propertyPairs { + let keyValue = pair.split(separator: "=", maxSplits: 1) + if keyValue.count == 2 { + let key = String(keyValue[0]) + let value = String(keyValue[1]) + properties[key] = value + } + } + + var jsonString = "" + do { + let jsonData = try JSONSerialization.data(withJSONObject: properties, options: [.prettyPrinted]) + jsonString = String(data: jsonData, encoding: .utf8)! + } catch { + // nothing + } + let client = HTTPClient(username: Preferences.username, password: Preferences.username) + client.doPost(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], path: "/rest/rules/rules/\(uuid)/runnow", body: jsonString) { data, _, error in + if let error { + os_log("Could not send data %{public}@", log: .default, type: .error, error.localizedDescription) + } else { + os_log("Request succeeded", log: .default, type: .info) + if let data { + os_log("Data: %{public}@", log: .default, type: .debug, String(data: data, encoding: .utf8) ?? "") + } + } + } + } + func showSideMenu() { os_log("OpenHABRootViewController showSideMenu", log: .viewCycle, type: .info) if let menu = SideMenuManager.default.rightMenuNavigationController { diff --git a/openHABIntents/openHABIntents.entitlements b/openHABIntents/openHABIntents.entitlements index b6063e1b..029463f3 100644 --- a/openHABIntents/openHABIntents.entitlements +++ b/openHABIntents/openHABIntents.entitlements @@ -6,5 +6,9 @@ group.org.openhab.app + keychain-access-groups + + $(AppIdentifierPrefix)org.openhab.app + diff --git a/openHABWatch Extension/openHABWatch Extension.entitlements b/openHABWatch Extension/openHABWatch Extension.entitlements index b6063e1b..e38182d1 100644 --- a/openHABWatch Extension/openHABWatch Extension.entitlements +++ b/openHABWatch Extension/openHABWatch Extension.entitlements @@ -6,5 +6,9 @@ group.org.openhab.app + keychain-access-groups + + $(AppIdentifierPrefix)org.openhab.app.watchkitapp +