atproto_devtool/commands/test/
labeler.rs1pub mod create_report;
4pub mod crypto;
5pub mod http;
6pub mod identity;
7pub mod pipeline;
8pub mod report;
9pub mod subscription;
10
11use std::io;
12use std::process::ExitCode;
13use std::time::Duration;
14
15use clap::Args;
16use miette::Report;
17
18use crate::commands::test::labeler::create_report::self_mint::{SelfMintCurve, SelfMintSigner};
19use crate::common::{
20 APP_USER_AGENT,
21 identity::{Did, RealDnsResolver, RealHttpClient, is_local_labeler_hostname},
22};
23use pipeline::{LabelerOptions, parse_target, run_pipeline};
24use report::RenderConfig;
25
26#[derive(Debug, Args)]
28pub struct LabelerCmd {
29 pub target: String,
31
32 #[arg(long)]
35 pub did: Option<String>,
36
37 #[arg(
41 long,
42 default_value = "5s",
43 value_parser = parse_subscribe_timeout,
44 )]
45 pub subscribe_timeout: Duration,
46
47 #[arg(long)]
49 pub no_color: bool,
50
51 #[arg(long)]
53 pub verbose: bool,
54
55 #[arg(long)]
59 pub commit_report: bool,
60
61 #[arg(long)]
65 pub force_self_mint: bool,
66
67 #[arg(long, value_enum, default_value_t = SelfMintCurve::default())]
69 pub self_mint_curve: SelfMintCurve,
70
71 #[arg(long)]
75 pub report_subject_did: Option<String>,
76
77 #[arg(long, requires = "app_password")]
81 pub handle: Option<String>,
82
83 #[arg(long, requires = "handle")]
86 pub app_password: Option<String>,
87}
88
89impl LabelerCmd {
90 pub async fn run(self, no_color: bool) -> Result<ExitCode, Report> {
91 let target =
93 parse_target(&self.target, self.did.as_deref()).map_err(|e| miette::miette!("{e}"))?;
94
95 let tentative_endpoint: Option<url::Url> = match &target {
100 pipeline::LabelerTarget::Endpoint { url, .. } => Some(url.clone()),
101 pipeline::LabelerTarget::Identified { .. } => None,
102 };
103
104 let tentative_local = tentative_endpoint
105 .as_ref()
106 .map(is_local_labeler_hostname)
107 .unwrap_or(false);
108
109 let reqwest_client = reqwest::Client::builder()
111 .use_rustls_tls()
112 .user_agent(APP_USER_AGENT)
113 .timeout(std::time::Duration::from_secs(10))
114 .build()
115 .map_err(|e| miette::miette!("Failed to initialize HTTP client: {}", e))?;
116
117 let http = RealHttpClient::from_client(reqwest_client.clone());
119 let dns = RealDnsResolver::new();
120
121 let report_subject_override = self.report_subject_did.clone().map(Did);
123
124 let run_id = create_report::sentinel::new_run_id();
126
127 let self_mint_signer_opt = if self.force_self_mint || tentative_local {
133 Some(
134 SelfMintSigner::spawn(self.self_mint_curve)
135 .await
136 .map_err(|e| miette::miette!("Failed to bind self-mint DID server: {e}"))?,
137 )
138 } else {
139 None
140 };
141 let self_mint_signer_ref = self_mint_signer_opt.as_ref();
142
143 let pds_credentials = match (self.handle.as_deref(), self.app_password.as_deref()) {
145 (Some(h), Some(p)) => Some(pipeline::PdsCredentials {
146 handle: h.to_string(),
147 app_password: p.to_string(),
148 }),
149 _ => None,
150 };
151 let pds_credentials_ref = pds_credentials.as_ref();
152
153 let opts = LabelerOptions {
155 http: &http,
156 dns: &dns,
157 http_tee: pipeline::HttpTee::Real(&reqwest_client),
158 ws_client: None,
159 subscribe_timeout: self.subscribe_timeout,
160 verbose: self.verbose,
161
162 create_report_tee: pipeline::CreateReportTeeKind::Real(&reqwest_client),
163 commit_report: self.commit_report,
164 force_self_mint: self.force_self_mint,
165 self_mint_curve: self.self_mint_curve,
166 report_subject_override: report_subject_override.as_ref(),
167 self_mint_signer: self_mint_signer_ref,
168 pds_credentials: pds_credentials_ref,
169 pds_xrpc_client: None,
170 pds_xrpc_client_override: None,
171 run_id: &run_id,
172 };
173
174 let report = run_pipeline(target, opts).await;
175
176 let mut stdout = io::stdout().lock();
178 report
179 .render(&mut stdout, &RenderConfig { no_color })
180 .map_err(|e| miette::miette!("Failed to render report: {}", e))?;
181
182 let exit_code = report.exit_code();
184 Ok(ExitCode::from(exit_code as u8))
185 }
186}
187
188pub fn parse_subscribe_timeout(raw: &str) -> Result<Duration, String> {
189 let parsed = humantime::parse_duration(raw)
190 .map_err(|e| format!("invalid --subscribe-timeout value `{raw}`: {e}"))?;
191
192 if parsed < Duration::from_secs(1) {
193 return Err(format!(
194 "--subscribe-timeout must be at least 1 second (got {parsed:?})"
195 ));
196 }
197
198 Ok(parsed)
199}