krz/domain-dig

an ios app for DNS & SSL analysis

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

v4.6.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 var title: LocalizedStringResource = "Inspect Domain"
 10    static var 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 var 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    static func summaryText(for report: DomainReport) -> String {
 48        let dnssec: String
 49        switch report.dns.dnssecSigned {
 50        case true?: dnssec = "Yes"
 51        case false?: dnssec = "No"
 52        case nil: dnssec = "Unknown"
 53        }
 54
 55        var lines = [
 56            "\(report.domain)\(report.availability.rawValue.capitalized)",
 57            "Risk: \(report.riskAssessment.level.title) (score \(report.riskAssessment.score))",
 58            "Health: \(report.health.title)",
 59            "TLS: \(report.web.tlsGrade.rawValue) · Email: \(report.email.grade?.rawValue ?? "") · Cert: \(report.certificateExpiryState.title)",
 60            "IP: \(report.dns.primaryIP ?? "unknown") · DNSSEC: \(dnssec)"
 61        ]
 62
 63        if let insight = report.insights.first {
 64            lines.append(insight)
 65        }
 66
 67        return lines.joined(separator: "\n")
 68    }
 69
 70    /// Short spoken/dialog line for Siri and the Shortcuts result banner.
 71    static func spokenSummary(for report: DomainReport) -> String {
 72        "\(report.domain) is \(report.availability.rawValue). Risk \(report.riskAssessment.level.title.lowercased()), health \(report.health.title.lowercased())."
 73    }
 74}
 75
 76enum InspectDomainError: Error, CustomLocalizedStringResourceConvertible {
 77    case emptyDomain
 78
 79    var localizedStringResource: LocalizedStringResource {
 80        switch self {
 81        case .emptyDomain:
 82            return "Enter a domain to inspect."
 83        }
 84    }
 85}
 86
 87/// App Intent that opens DomainDig and adds a domain to the watchlist. It opens
 88/// the app via the `domaindig://watch` deep link so tracking goes through the
 89/// existing view-model path (premium limits, monitoring, history linking,
 90/// cloud-sync recording, and the paywall when over the free limit).
 91struct AddToWatchlistIntent: AppIntent {
 92    static var title: LocalizedStringResource = "Add Domain to Watchlist"
 93    static var description = IntentDescription(
 94        "Open DomainDig and add a domain to your watchlist."
 95    )
 96
 97    static var openAppWhenRun = true
 98
 99    @Parameter(
100        title: "Domain",
101        description: "The domain to add, e.g. example.com",
102        inputOptions: String.IntentInputOptions(
103            keyboardType: .URL,
104            capitalizationType: .none
105        )
106    )
107    var domain: String
108
109    static var parameterSummary: some ParameterSummary {
110        Summary("Add \(\.$domain) to the watchlist")
111    }
112
113    @MainActor
114    func perform() async throws -> some IntentResult {
115        let requested = domain.trimmingCharacters(in: .whitespacesAndNewlines)
116        guard !requested.isEmpty else {
117            throw InspectDomainError.emptyDomain
118        }
119
120        // `openAppWhenRun` runs this in the app process, so the router hands the
121        // action off to the running UI, which tracks through the existing path.
122        DomainDigIntentRouter.shared.pendingAction = .watch(requested)
123        return .result()
124    }
125}
126
127/// App Intent that opens DomainDig and re-inspects every tracked domain. It runs
128/// through the existing view-model batch path (`refreshAllTrackedDomains`), which
129/// enforces the batch feature gate and surfaces the paywall when needed.
130struct RunSweepIntent: AppIntent {
131    static var title: LocalizedStringResource = "Run Watchlist Sweep"
132    static var description = IntentDescription(
133        "Open DomainDig and re-inspect every domain on your watchlist."
134    )
135
136    static var openAppWhenRun = true
137
138    @MainActor
139    func perform() async throws -> some IntentResult {
140        DomainDigIntentRouter.shared.pendingAction = .sweep
141        return .result()
142    }
143}
144
145/// In-process hand-off from an `openAppWhenRun` intent to the running SwiftUI
146/// layer. `RootTabView` observes `pendingAction` and performs it.
147@MainActor
148@Observable
149final class DomainDigIntentRouter {
150    static let shared = DomainDigIntentRouter()
151    var pendingAction: DomainDigDeepLink.Action?
152    private init() {}
153}
154
155/// Exposes DomainDig intents to Spotlight and Siri with invocation phrases.
156struct DomainDigShortcuts: AppShortcutsProvider {
157    static var appShortcuts: [AppShortcut] {
158        AppShortcut(
159            intent: InspectDomainIntent(),
160            phrases: [
161                "Inspect a domain with \(.applicationName)",
162                "Dig a domain with \(.applicationName)"
163            ],
164            shortTitle: "Inspect Domain",
165            systemImageName: "magnifyingglass"
166        )
167        AppShortcut(
168            intent: AddToWatchlistIntent(),
169            phrases: [
170                "Add a domain to \(.applicationName)",
171                "Watch a domain with \(.applicationName)"
172            ],
173            shortTitle: "Add to Watchlist",
174            systemImageName: "plus.circle"
175        )
176        AppShortcut(
177            intent: RunSweepIntent(),
178            phrases: [
179                "Run a sweep with \(.applicationName)",
180                "Sweep my \(.applicationName) watchlist"
181            ],
182            shortTitle: "Run Sweep",
183            systemImageName: "arrow.trianglehead.2.clockwise"
184        )
185    }
186}