secretive/Sources/SecretAgent/Notifier.swift

204 lines
10 KiB
Swift
Raw Normal View History

2020-03-04 07:14:38 +00:00
import Foundation
import UserNotifications
2020-03-22 00:52:51 +00:00
import AppKit
2020-03-22 01:43:26 +00:00
import SecretKit
import SecretAgentKit
import Brief
2025-01-06 00:07:11 +00:00
import Synchronization
2020-03-04 07:14:38 +00:00
2025-01-06 00:07:11 +00:00
final class Notifier: Sendable {
2020-03-04 07:14:38 +00:00
2020-05-16 06:19:00 +00:00
private let notificationDelegate = NotificationDelegate()
2020-03-22 01:43:26 +00:00
init() {
let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: String(localized: "update_notification_update_button"), options: [])
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: String(localized: "update_notification_ignore_button"), options: [])
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
2020-09-22 06:12:50 +00:00
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
let rawDurations = [
Measurement(value: 1, unit: UnitDuration.minutes),
Measurement(value: 5, unit: UnitDuration.minutes),
Measurement(value: 1, unit: UnitDuration.hours),
Measurement(value: 24, unit: UnitDuration.hours)
]
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: "persist_authentication_decline_button"), options: [])
var allPersistenceActions = [doNotPersistAction]
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
for duration in rawDurations {
let seconds = duration.converted(to: .seconds).value
guard let string = formatter.string(from: seconds)?.capitalized else { continue }
let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)")
let action = UNNotificationAction(identifier: identifier, title: string, options: [])
2025-01-06 00:07:11 +00:00
notificationDelegate.state.withLock { state in
state.persistOptions[identifier] = seconds
}
allPersistenceActions.append(action)
}
let persistAuthenticationCategory = UNNotificationCategory(identifier: Constants.persistAuthenticationCategoryIdentitifier, actions: allPersistenceActions, intentIdentifiers: [], options: [])
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
persistAuthenticationCategory.setValue(String(localized: "persist_authentication_accept_button"), forKey: "_actionsMenuTitle")
}
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
2020-03-22 01:43:26 +00:00
UNUserNotificationCenter.current().delegate = notificationDelegate
2025-01-06 00:07:11 +00:00
notificationDelegate.state.withLock { state in
state.persistAuthentication = { secret, store, duration in
guard let duration = duration else { return }
try? await store.persistAuthentication(secret: secret, forDuration: duration)
}
}
2020-03-22 01:43:26 +00:00
}
2020-03-04 07:14:38 +00:00
func prompt() {
let notificationCenter = UNUserNotificationCenter.current()
2020-09-22 06:12:50 +00:00
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
2020-03-04 07:14:38 +00:00
}
2024-12-25 23:25:01 +00:00
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
2025-01-06 00:07:11 +00:00
notificationDelegate.state.withLock { state in
state.pendingPersistableSecrets[secret.id.description] = secret
state.pendingPersistableStores[store.id.description] = store
}
2020-03-04 07:14:38 +00:00
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)")
notificationContent.subtitle = String(localized: "signed_notification_description_\(secret.name)")
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive
2024-12-25 23:25:01 +00:00
if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.requiresAuthentication {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
}
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
2020-03-22 00:52:51 +00:00
notificationContent.attachments = [attachment]
}
2020-03-04 07:14:38 +00:00
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
2025-01-06 00:07:11 +00:00
try? await notificationCenter.add(request)
2020-03-04 07:14:38 +00:00
}
func notify(update: Release, ignore: ((Release) -> Void)?) {
2025-01-06 00:07:11 +00:00
notificationDelegate.state.withLock { [update] state in
state.release = update
// state.ignore = ignore
}
2020-03-22 01:43:26 +00:00
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
if update.critical {
notificationContent.interruptionLevel = .critical
notificationContent.title = String(localized: "update_notification_update_critical_title_\(update.name)")
2020-03-22 01:43:26 +00:00
} else {
notificationContent.title = String(localized: "update_notification_update_normal_title_\(update.name)")
2020-03-22 01:43:26 +00:00
}
notificationContent.subtitle = String(localized: "update_notification_update_description")
2020-03-22 01:43:26 +00:00
notificationContent.body = update.body
notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
2020-03-22 01:43:26 +00:00
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
notificationCenter.add(request, withCompletionHandler: nil)
}
2020-03-04 07:14:38 +00:00
}
extension Notifier: SigningWitness {
2024-12-25 23:25:01 +00:00
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
2020-03-19 03:04:24 +00:00
}
2024-12-25 23:25:01 +00:00
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
await notify(accessTo: secret, from: store, by: provenance)
}
}
2020-03-22 01:43:26 +00:00
extension Notifier {
enum Constants {
// Update notifications
static let updateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update"
static let criticalUpdateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
static let updateActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
static let ignoreActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.ignoreaction"
// Authorization persistence notificatoins
static let persistAuthenticationCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication"
static let doNotPersistActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.donotpersist"
static let persistForActionIdentitifierPrefix = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.persist."
static let persistSecretIDKey = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.secretidkey"
static let persistStoreIDKey = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.storeidkey"
2020-03-22 01:43:26 +00:00
}
}
2025-01-06 00:07:11 +00:00
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
struct State {
typealias PersistAuthentication = ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)
typealias Ignore = ((Release) -> Void)
fileprivate var release: Release?
fileprivate var ignore: Ignore?
fileprivate var persistAuthentication: PersistAuthentication?
fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
}
fileprivate let state: Mutex<State> = .init(.init())
2025-01-06 00:07:11 +00:00
2020-03-22 01:43:26 +00:00
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
}
2024-12-25 23:25:01 +00:00
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
let category = response.notification.request.content.categoryIdentifier
switch category {
case Notifier.Constants.updateCategoryIdentitifier:
handleUpdateResponse(response: response)
case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
2024-12-25 23:25:01 +00:00
await handlePersistAuthenticationResponse(response: response)
default:
2022-06-04 22:25:13 +00:00
break
}
}
func handleUpdateResponse(response: UNNotificationResponse) {
2025-01-06 00:07:11 +00:00
state.withLock { state in
guard let update = state.release else { return }
switch response.actionIdentifier {
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
NSWorkspace.shared.open(update.html_url)
case Notifier.Constants.ignoreActionIdentitifier:
state.ignore?(update)
default:
fatalError()
}
}
}
2024-12-25 23:25:01 +00:00
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
2025-01-06 00:07:11 +00:00
// let (secret, store, persistOptions, callback): (AnySecret?, AnySecretStore?, TimeInterval?, State.PersistAuthentication?) = state.withLock { state in
// guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = state.pendingPersistableSecrets[secretID],
// let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = state.pendingPersistableStores[storeID]
// else { return (nil, nil, nil, nil) }
// state.pendingPersistableSecrets[secretID] = nil
// return (secret, store, state.persistOptions[response.actionIdentifier], state.persistAuthentication)
// }
// guard let secret, let store, let persistOptions else { return }
// await callback?(secret, store, persistOptions)
2020-03-22 01:43:26 +00:00
}
2025-01-06 00:07:11 +00:00
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
[.list, .banner]
2020-03-22 01:43:26 +00:00
}
}