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