krz/domain-dig

an ios app for DNS & SSL analysis

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

v4.9.0: DomainDig/DomainDigIntents.swift · raw

  1import AppIntents
  2import Foundation
  3
  4/// App Intent that runs a point-in-time domain inspection through the same
  5/// headless pipeline used by the CLI (`DomainInspectionService` ->
  6/// `DomainReportBuilder`) and returns a concise summary. Usable from
  7/// Shortcuts, Spotlight, the Action button, and Siri.
  8struct InspectDomainIntent: AppIntent {
  9    static let title: LocalizedStringResource = "Inspect Domain"
 10    static let description = IntentDescription(
 11        "Run a DomainDig inspection and return a summary of availability, risk, TLS, email security, and certificate health."
 12    )
 13
 14    // Read-only inspection; no need to foreground the app.
 15    static let openAppWhenRun = false
 16
 17    @Parameter(
 18        title: "Domain",
 19        description: "The domain to inspect, e.g. example.com",
 20        inputOptions: String.IntentInputOptions(
 21            keyboardType: .URL,
 22            capitalizationType: .none
 23        )
 24    )
 25    var domain: String
 26
 27    static var parameterSummary: some ParameterSummary {
 28        Summary("Inspect \(\.$domain)")
 29    }
 30
 31    @MainActor
 32    func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
 33        let requested = domain.trimmingCharacters(in: .whitespacesAndNewlines)
 34        guard !requested.isEmpty else {
 35            throw InspectDomainError.emptyDomain
 36        }
 37
 38        let snapshot = await DomainInspectionService().inspectSnapshot(domain: requested)
 39        let report = DomainReportBuilder().build(from: snapshot)
 40
 41        let summary = Self.summaryText(for: report)
 42        let dialog = IntentDialog(stringLiteral: Self.spokenSummary(for: report))
 43        return .result(value: summary, dialog: dialog)
 44    }
 45
 46    /// Multi-line summary suitable for a returned Shortcuts text value.
 47    @MainActor
 48    static func summaryText(for report: DomainReport) -> String {
 49        let dnssec: String
 50        switch report.dns.dnssecSigned {
 51        case true?: dnssec = "Yes"
 52        case false?: dnssec = "No"
 53        case nil: dnssec = "Unknown"
 54        }
 55
 56        var lines = [
 57            "\(report.domain)\(report.availability.rawValue.capitalized)",
 58            "Risk: \(report.riskAssessment.level.title) (score \(report.riskAssessment.score))",
 59            "Health: \(report.health.title)",
 60            "TLS: \(report.web.tlsGrade.rawValue) · Email: \(report.email.grade?.rawValue ?? "") · Cert: \(report.certificateExpiryState.title)",
 61            "IP: \(report.dns.primaryIP ?? "unknown") · DNSSEC: \(dnssec)"
 62        ]
 63
 64        if let insight = report.insights.first {
 65            lines.append(insight)
 66        }
 67
 68        return lines.joined(separator: "\n")
 69    }
 70
 71    /// Short spoken/dialog line for Siri and the Shortcuts result banner.
 72    @MainActor
 73    static func spokenSummary(for report: DomainReport) -> String {
 74        "\(report.domain) is \(report.availability.rawValue). Risk \(report.riskAssessment.level.title.lowercased()), health \(report.health.title.lowercased())."
 75    }
 76}
 77
 78enum InspectDomainError: Error, CustomLocalizedStringResourceConvertible {
 79    case emptyDomain
 80
 81    var localizedStringResource: LocalizedStringResource {
 82        switch self {
 83        case .emptyDomain:
 84            return "Enter a domain to inspect."
 85        }
 86    }
 87}
 88
 89/// App Intent that opens DomainDig and adds a domain to the watchlist. It opens
 90/// the app via the `domaindig://watch` deep link so tracking goes through the
 91/// existing view-model path (premium limits, monitoring, history linking,
 92/// cloud-sync recording, and the paywall when over the free limit).
 93struct AddToWatchlistIntent: AppIntent {
 94    static let title: LocalizedStringResource = "Add Domain to Watchlist"
 95    static let description = IntentDescription(
 96        "Open DomainDig and add a domain to your watchlist."
 97    )
 98
 99    static let openAppWhenRun = true
100
101    @Parameter(
102        title: "Domain",
103        description: "The domain to add, e.g. example.com",
104        inputOptions: String.IntentInputOptions(
105            keyboardType: .URL,
106            capitalizationType: .none
107        )
108    )
109    var domain: String
110
111    static var parameterSummary: some ParameterSummary {
112        Summary("Add \(\.$domain) to the watchlist")
113    }
114
115    @MainActor
116    func perform() async throws -> some IntentResult {
117        let requested = domain.trimmingCharacters(in: .whitespacesAndNewlines)
118        guard !requested.isEmpty else {
119            throw InspectDomainError.emptyDomain
120        }
121
122        // `openAppWhenRun` runs this in the app process, so the router hands the
123        // action off to the running UI, which tracks through the existing path.
124        DomainDigIntentRouter.shared.pendingAction = .watch(requested)
125        return .result()
126    }
127}
128
129/// App Intent that opens DomainDig and re-inspects every tracked domain. It runs
130/// through the existing view-model batch path (`refreshAllTrackedDomains`), which
131/// enforces the batch feature gate and surfaces the paywall when needed.
132struct RunSweepIntent: AppIntent {
133    static let title: LocalizedStringResource = "Run Watchlist Sweep"
134    static let description = IntentDescription(
135        "Open DomainDig and re-inspect every domain on your watchlist."
136    )
137
138    static let openAppWhenRun = true
139
140    @MainActor
141    func perform() async throws -> some IntentResult {
142        DomainDigIntentRouter.shared.pendingAction = .sweep
143        return .result()
144    }
145}
146
147/// In-process hand-off from an `openAppWhenRun` intent to the running SwiftUI
148/// layer. `RootTabView` observes `pendingAction` and performs it.
149@MainActor
150@Observable
151final class DomainDigIntentRouter {
152    static let shared = DomainDigIntentRouter()
153    var pendingAction: DomainDigDeepLink.Action?
154    private init() { /* Singleton; use the shared instance. */ }
155}
156
157/// Exposes DomainDig intents to Spotlight and Siri with invocation phrases.
158struct DomainDigShortcuts: AppShortcutsProvider {
159    static var appShortcuts: [AppShortcut] {
160        AppShortcut(
161            intent: InspectDomainIntent(),
162            phrases: [
163                "Inspect a domain with \(.applicationName)",
164                "Dig a domain with \(.applicationName)"
165            ],
166            shortTitle: "Inspect Domain",
167            systemImageName: "magnifyingglass"
168        )
169        AppShortcut(
170            intent: AddToWatchlistIntent(),
171            phrases: [
172                "Add a domain to \(.applicationName)",
173                "Watch a domain with \(.applicationName)"
174            ],
175            shortTitle: "Add to Watchlist",
176            systemImageName: "plus.circle"
177        )
178        AppShortcut(
179            intent: RunSweepIntent(),
180            phrases: [
181                "Run a sweep with \(.applicationName)",
182                "Sweep my \(.applicationName) watchlist"
183            ],
184            shortTitle: "Run Sweep",
185            systemImageName: "arrow.trianglehead.2.clockwise"
186        )
187    }
188}