Skip to content

Commit

Permalink
Merged develop. Changed preferences view to show settings.sitemapForW…
Browse files Browse the repository at this point in the history
…atch

Signed-off-by: Tim Müller-Seydlitz <[email protected]>
  • Loading branch information
timbms committed Jul 28, 2024
2 parents 8698feb + 111e39b commit a0e763d
Show file tree
Hide file tree
Showing 50 changed files with 1,578 additions and 1,081 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ on:

jobs:
build_and_deploy:
runs-on: macos-13
runs-on: macos-14

steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
token: ${{ secrets.ACCESS_TOKEN }}
fetch-depth: 0
Expand All @@ -36,7 +36,7 @@ jobs:
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
- name: SSH Keys
uses: webfactory/ssh-agent@v0.7.0
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: |
${{ secrets.MATCH_GIT_PRIVATE_KEY }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pull_requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ on:

jobs:
build_and_test:
runs-on: macos-13
runs-on: macos-14
if: ${{ github.event.pull_request.draft == false }}
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install dependencies
run: |
Expand All @@ -25,6 +25,6 @@ jobs:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
uses: maierj/fastlane-action@v2.3.0
uses: maierj/fastlane-action@v3.1.0
with:
lane: unittests
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [Version 3.0.1, Build 5] - 2024-07-07Z

- Prefer commandDescription options over stateDescription options if an item has them

## [Version 2.4.59, Build 1580410537] - 2023-08-19Z
Expand Down
28 changes: 14 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ GEM
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.943.0)
aws-sdk-core (3.197.0)
aws-partitions (1.952.0)
aws-sdk-core (3.201.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.83.0)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.152.1)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.156.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
Expand Down Expand Up @@ -68,7 +68,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.220.0)
fastlane (2.221.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
Expand Down Expand Up @@ -154,9 +154,9 @@ GEM
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
jwt (2.8.2)
base64
mini_magick (4.12.0)
mini_magick (4.13.1)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
Expand All @@ -166,7 +166,7 @@ GEM
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.5)
public_suffix (6.0.0)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
Expand Down
13 changes: 13 additions & 0 deletions NotificationService/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>
14 changes: 14 additions & 0 deletions NotificationService/NotificationService.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.openhab.app</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)org.openhab.app</string>
</array>
</dict>
</plist>
229 changes: 229 additions & 0 deletions NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright (c) 2010-2024 Contributors to the openHAB project
//
// See the NOTICE file(s) distributed with this work for additional
// information.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

import Foundation
import OpenHABCore
import os.log
import UniformTypeIdentifiers
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 {
if let action = actionDict["action"],
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") || action.hasPrefix("app") {
options = [.foreground]
}
let notificationAction = UNNotificationAction(
identifier: action,
title: title,
options: options
)
notificationActions.append(notificationAction)
}
}
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
)
UNUserNotificationCenter.current().getNotificationCategories { existingCategories in
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)
bestAttemptContent.attachments = [attachment]
} else {
os_log("handleNotification could not attach %{PUBLIC}@", log: .default, type: .info, attachmentURLString)
}
contentHandler(bestAttemptContent)
}

if isItem {
downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler)
} else {
downloadAndAttachMedia(url: attachmentURLString, completion: downloadCompletionHandler)
}
} else {
contentHandler(bestAttemptContent)
}
}
}

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.
os_log("serviceExtensionTimeWillExpire", log: .default, type: .info)
if let contentHandler, let bestAttemptContent {
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) {
do {
if let actionsArray = try JSONSerialization.jsonObject(with: actionsData, options: []) as? [[String: String]] {
return actionsArray
}
} catch {
os_log("Error parsing actions: %{PUBLIC}@", log: .default, type: .info, error.localizedDescription)
}
}
return nil
}

private func parseCategory(_ userInfo: [AnyHashable: Any]) -> String? {
// Extract category from aps dictionary
if let aps = userInfo["aps"] as? [String: Any],
let category = aps["category"] as? String {
return category
}
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")
completion(nil)
return
}
self.attachFile(localURL: localURL, mimeType: response?.mimeType, completion: completion)
}
if url.starts(with: "/") {
client.downloadFile(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], path: url, completionHandler: downloadCompletionHandler)
} else {
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 {
os_log("Could not find item %{PUBLIC}@", log: .default, type: .info, itemName)
completion(nil)
return
}
if let state = item.state {
do {
// Extract MIME type and base64 string
let pattern = "^data:(.*?);base64,(.*)$"
let regex = try NSRegularExpression(pattern: pattern, options: [])
if let match = regex.firstMatch(in: state, options: [], range: NSRange(location: 0, length: state.utf16.count)) {
let mimeTypeRange = Range(match.range(at: 1), in: state)
let base64Range = Range(match.range(at: 2), in: state)
if let mimeTypeRange, let base64Range {
let mimeType = String(state[mimeTypeRange])
let base64String = String(state[base64Range])
if let imageData = Data(base64Encoded: base64String) {
// Create a temporary file URL
let tempDirectory = FileManager.default.temporaryDirectory
let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString)
do {
try imageData.write(to: tempFileURL)
os_log("Image saved to temporary file: %{PUBLIC}@", log: .default, type: .info, tempFileURL.absoluteString)
self.attachFile(localURL: tempFileURL, mimeType: mimeType, completion: completion)
return
} catch {
os_log("Failed to write image data to file: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription)
}
} else {
os_log("Failed to decode base64 string to Data", log: .default, type: .error)
}
}
}
} catch {
os_log("Failed to parse data: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription)
}
}
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) {
let newTempFile = tempFile.appendingPathExtension(utType.preferredFilenameExtension ?? "")
try fileManager.moveItem(at: tempFile, to: newTempFile)
attachment = try UNNotificationAttachment(identifier: UUID().uuidString, url: newTempFile, options: nil)
} else {
os_log("Unrecognized MIME type or file extension", log: .default, type: .error)
attachment = nil
}
completion(attachment)
return
} catch {
os_log("Failed to create UNNotificationAttachment: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription)
}
completion(nil)
}
}
2 changes: 0 additions & 2 deletions OpenHABCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(name: "Alamofire", url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"),
.package(name: "Fuzi", url: "https://github.com/cezheng/Fuzi.git", from: "3.0.0"),
.package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0")
],
targets: [
Expand All @@ -26,7 +25,6 @@ let package = Package(
name: "OpenHABCore",
dependencies: [
.product(name: "Alamofire", package: "Alamofire", condition: .when(platforms: [.iOS, .watchOS])),
.product(name: "Fuzi", package: "Fuzi", condition: .when(platforms: [.iOS, .watchOS])),
.product(name: "Kingfisher", package: "Kingfisher", condition: .when(platforms: [.iOS, .watchOS]))
]
),
Expand Down
Loading

0 comments on commit a0e763d

Please sign in to comment.