forked from Manuel/meeting-assistant
116 lines
4.4 KiB
Swift
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 }
|
|
}
|
|
}
|