krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v4.5.0: DomainDig/LocalNotificationService.swift · raw

  1import Foundation
  2import UserNotifications
  3
  4@MainActor
  5final class LocalNotificationService {
  6    static let shared = LocalNotificationService()
  7
  8    private init() {}
  9
 10    func configureForegroundPresentation() {
 11        UNUserNotificationCenter.current().delegate = NotificationCenterDelegate.shared
 12    }
 13
 14    func requestAuthorizationIfNeeded() async -> Bool {
 15        let center = UNUserNotificationCenter.current()
 16        let settings = await center.notificationSettings()
 17
 18        switch settings.authorizationStatus {
 19        case .authorized, .provisional, .ephemeral:
 20            return true
 21        case .notDetermined:
 22            return (try? await center.requestAuthorization(options: [.alert, .badge, .sound])) ?? false
 23        case .denied:
 24            return false
 25        @unknown default:
 26            return false
 27        }
 28    }
 29
 30    func isAuthorizedForAlerts() async -> Bool {
 31        let settings = await UNUserNotificationCenter.current().notificationSettings()
 32        switch settings.authorizationStatus {
 33        case .authorized, .provisional, .ephemeral:
 34            return true
 35        case .denied, .notDetermined:
 36            return false
 37        @unknown default:
 38            return false
 39        }
 40    }
 41
 42    func notifyDomainEvent(domain: String, message: String, severity: ChangeSeverity) async {
 43        await schedule(
 44            identifier: "domain-change-\(domain)",
 45            title: domain,
 46            body: message,
 47            interruptionLevel: severity == .high ? .timeSensitive : .active
 48        )
 49    }
 50
 51    func notifyCertificateWarning(domain: String, daysRemaining: Int) async {
 52        await schedule(
 53            identifier: "cert-warning-\(domain)",
 54            title: domain,
 55            body: "Certificate expires in \(daysRemaining) days",
 56            interruptionLevel: .timeSensitive
 57        )
 58    }
 59
 60    func notifyMonitoringAlert(
 61        domain: String,
 62        message: String,
 63        severity: MonitoringAlertSeverity
 64    ) async {
 65        let interruptionLevel: UNNotificationInterruptionLevel
 66        switch severity {
 67        case .critical:
 68            interruptionLevel = .timeSensitive
 69        case .warning, .info:
 70            interruptionLevel = .active
 71        }
 72
 73        await schedule(
 74            identifier: "monitoring-\(domain)-\(UUID().uuidString)",
 75            title: domain,
 76            body: message,
 77            interruptionLevel: interruptionLevel
 78        )
 79    }
 80
 81    func notifyMonitoringSummary(
 82        domain: String,
 83        alerts: [MonitoringPendingAlert]
 84    ) async {
 85        let summary = alerts
 86            .sorted { $0.detectedAt < $1.detectedAt }
 87            .prefix(2)
 88            .map(\.message)
 89            .joined(separator: "")
 90        let body: String
 91        if alerts.count <= 1 {
 92            body = alerts.first?.message ?? "Monitoring change detected"
 93        } else if summary.isEmpty {
 94            body = "\(alerts.count) monitoring changes detected"
 95        } else {
 96            body = "\(alerts.count) monitoring changes: \(summary)"
 97        }
 98
 99        let severity = alerts.map(\.severity).max() ?? .info
100        let interruptionLevel: UNNotificationInterruptionLevel = severity == .critical ? .timeSensitive : .active
101
102        await schedule(
103            identifier: "monitoring-summary-\(domain)-\(UUID().uuidString)",
104            title: domain,
105            body: body,
106            interruptionLevel: interruptionLevel
107        )
108    }
109
110    func notifySweepComplete(summary: BatchSweepSummary) async {
111        let body = "\(summary.changedDomains) changed, \(summary.warningDomains) warnings, \(summary.unchangedDomains) unchanged"
112        await schedule(
113            identifier: "sweep-complete",
114            title: summary.source == .watchlistRefresh ? "Check All Complete" : "Batch Complete",
115            body: body,
116            interruptionLevel: .active
117        )
118    }
119
120    func clearAllNotifications() async {
121        let center = UNUserNotificationCenter.current()
122        center.removeAllPendingNotificationRequests()
123        center.removeAllDeliveredNotifications()
124    }
125
126    private func schedule(
127        identifier: String,
128        title: String,
129        body: String,
130        interruptionLevel: UNNotificationInterruptionLevel
131    ) async {
132        let content = UNMutableNotificationContent()
133        content.title = title
134        content.body = body
135        content.sound = .default
136        content.interruptionLevel = interruptionLevel
137
138        let request = UNNotificationRequest(
139            identifier: identifier,
140            content: content,
141            trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
142        )
143
144        try? await UNUserNotificationCenter.current().add(request)
145    }
146}
147
148private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
149    static let shared = NotificationCenterDelegate()
150
151    func userNotificationCenter(
152        _ center: UNUserNotificationCenter,
153        willPresent notification: UNNotification
154    ) async -> UNNotificationPresentationOptions {
155        [.banner, .list, .sound]
156    }
157}