Files
meeting-assistant/MeetingAssistant/Native/MacOsDesktopControls/DesktopUserRequests.swift
T
dh 728e66dd72
PR and Push Build/Test / windows-build-and-test (push) Failing after 38m25s
PR and Push Build/Test / portable-build-and-test (push) Successful in 8m9s
PR and Push Build/Test / macos-native-full (push) Skipped
ci: consolidate platform pipelines and test prerequisites on macOS support
2026-10-06 09:02:19 +02:00

116 lines
4.4 KiB
Swift

import Foundation
import UserNotifications
struct DesktopUserAction: Decodable, Equatable {
let id: String
let label: String
}
struct DesktopUserRequest: Decodable {
let id: String
let kind: String
let title: String
let message: String
let createdAt: Date
let expiresAt: Date
let actions: [DesktopUserAction]
}
struct DesktopAgentWindow: Decodable {
let id: String
let url: String
}
func decodeDesktopRequests(_ data: Data) throws -> [DesktopUserRequest] {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let value = try decoder.singleValueContainer().decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: value) { return date }
formatter.formatOptions = [.withInternetDateTime]
if let date = formatter.date(from: value) { return date }
throw DecodingError.dataCorruptedError(in: try decoder.singleValueContainer(), debugDescription: "Invalid request timestamp")
}
return try decoder.decode([DesktopUserRequest].self, from: data)
}
struct DesktopUserRequestState {
private(set) var pending: [DesktopUserRequest] = []
private var announced: Set<String> = []
private var responding: Set<String> = []
private var completed: Set<String> = []
mutating func beginResponse(baseUrl: String, requestId: String, action: String, now: Date) -> URLRequest? {
guard !responding.contains(requestId),
let pendingRequest = pending.first(where: { $0.id == requestId }),
pendingRequest.expiresAt > now,
pendingRequest.actions.contains(where: { $0.id == action }),
let id = requestId.addingPercentEncoding(withAllowedCharacters: .alphanumerics.union(CharacterSet(charactersIn: "-._~"))),
let url = URL(string: baseUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/desktop/requests/\(id)/respond"),
let body = try? JSONEncoder().encode(["action": action]) else { return nil }
var response = URLRequest(url: url)
response.httpMethod = "POST"
response.setValue("application/json", forHTTPHeaderField: "Content-Type")
response.httpBody = body
responding.insert(requestId)
return response
}
mutating func completeResponse(_ id: String, accepted: Bool) {
responding.remove(id)
if accepted { completed.insert(id); pending.removeAll { $0.id == id } }
}
func isResponding(_ id: String) -> Bool { responding.contains(id) }
mutating func reconcile(_ requests: [DesktopUserRequest], now: Date) -> [String] {
let previousIds = Set(pending.map(\.id))
pending = requests.filter { $0.expiresAt > now && !completed.contains($0.id) }.sorted {
$0.createdAt == $1.createdAt ? $0.id < $1.id : $0.createdAt < $1.createdAt
}
let currentIds = Set(pending.map(\.id))
return previousIds.subtracting(currentIds).sorted()
}
func notificationsToAnnounce() -> [DesktopUserRequest] {
pending.filter { !announced.contains($0.id) && !responding.contains($0.id) }
}
mutating func markAnnounced(_ id: String) {
if pending.contains(where: { $0.id == id }) { announced.insert(id) }
}
}
extension DesktopUserRequest {
var notificationIdentifier: String { "desktop-request." + id }
func notificationCategory() -> UNNotificationCategory {
UNNotificationCategory(
identifier: notificationIdentifier,
actions: actions.map { UNNotificationAction(identifier: $0.id, title: $0.label, options: []) },
intentIdentifiers: [],
options: [.customDismissAction])
}
func notificationRequest() -> UNNotificationRequest {
let content = UNMutableNotificationContent()
content.title = title
content.body = message
content.categoryIdentifier = notificationIdentifier
content.userInfo = ["requestId": id]
content.sound = .default
return UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: nil)
}
}
struct DesktopAgentWindowState {
private var opened: Set<String> = []
mutating func takeUnopened(_ windows: [DesktopAgentWindow]) -> [DesktopAgentWindow] {
windows.filter { opened.insert($0.id).inserted }
}
}