krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.3: 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}