1use crate::types::{RequestLog, ResponseLog};
2use anyhow::Result;
3use log::debug;
4use reqwest::{Client, Url};
5use std::time::{Duration, Instant};
6
7pub struct SearchfoxClient {
8 client: Client,
9 pub repo: String,
10 pub log_requests: bool,
11 request_counter: std::sync::atomic::AtomicUsize,
12}
13
14impl SearchfoxClient {
15 pub fn new(repo: String, log_requests: bool) -> Result<Self> {
16 let client = Self::create_tls13_client()?;
17 Ok(Self {
18 client,
19 repo,
20 log_requests,
21 request_counter: std::sync::atomic::AtomicUsize::new(0),
22 })
23 }
24
25 fn create_tls13_client() -> Result<Client> {
26 Client::builder()
27 .user_agent(Self::get_user_agent())
28 .use_rustls_tls()
29 .min_tls_version(reqwest::tls::Version::TLS_1_2)
30 .max_tls_version(reqwest::tls::Version::TLS_1_3)
31 .timeout(Duration::from_secs(30))
32 .build()
33 .map_err(|e| anyhow::anyhow!("Failed to build TLS client with rustls: {}", e))
34 }
35
36 fn get_user_agent() -> String {
37 let magic_word = std::env::var("SEARCHFOX_MAGIC_WORD")
38 .unwrap_or_else(|_| "sésame ouvre toi".to_string());
39 format!("searchfox-cli/{} ({})", crate::VERSION, magic_word)
40 }
41
42 pub fn log_request_start(&self, method: &str, url: &str) -> Option<RequestLog> {
43 if !self.log_requests {
44 return None;
45 }
46
47 let request_id = self
48 .request_counter
49 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
50 + 1;
51
52 let log = RequestLog {
53 url: url.to_string(),
54 method: method.to_string(),
55 start_time: Instant::now(),
56 request_id,
57 };
58
59 eprintln!(
60 "[REQ-{}] {} {} - START",
61 log.request_id, log.method, log.url
62 );
63 Some(log)
64 }
65
66 pub fn log_request_end(&self, request_log: RequestLog, status: u16, size_bytes: usize) {
67 let duration = request_log.start_time.elapsed();
68
69 let response_log = ResponseLog {
70 request_id: request_log.request_id,
71 status,
72 size_bytes,
73 duration,
74 };
75
76 eprintln!(
77 "[REQ-{}] {} {} - END ({}ms, {} bytes, HTTP {})",
78 response_log.request_id,
79 request_log.method,
80 request_log.url,
81 duration.as_millis(),
82 size_bytes,
83 status
84 );
85 }
86
87 pub async fn ping(&self) -> Result<Duration> {
88 if !self.log_requests {
89 return Ok(Duration::from_millis(0));
90 }
91
92 eprintln!(
93 "[PING] Testing network latency to searchfox.org (ICMP ping disabled, using HTTP HEAD)..."
94 );
95
96 let ping_url = "https://searchfox.org/";
97 let start = Instant::now();
98
99 let response = self
100 .client
101 .head(ping_url)
102 .timeout(Duration::from_secs(10))
103 .send()
104 .await?;
105
106 let latency = start.elapsed();
107
108 eprintln!(
109 "[PING] HTTP HEAD latency: {}ms (HTTP {})",
110 latency.as_millis(),
111 response.status()
112 );
113 eprintln!(
114 "[PING] Note: This includes minimal server processing time, not just network latency"
115 );
116
117 Ok(latency)
118 }
119
120 pub async fn get(&self, url: Url) -> Result<reqwest::Response> {
121 let request_log = self.log_request_start("GET", url.as_ref());
122 let response = self
123 .client
124 .get(url.clone())
125 .header("Accept", "application/json")
126 .send()
127 .await?;
128
129 if let Some(req_log) = request_log {
130 self.log_request_end(req_log, response.status().as_u16(), 0);
131 }
132
133 Ok(response)
134 }
135
136 pub async fn get_raw(&self, url: &str) -> Result<String> {
137 let request_log = self.log_request_start("GET", url);
138 let response = self.client.get(url).send().await?;
139
140 if !response.status().is_success() {
141 if let Some(req_log) = request_log {
142 self.log_request_end(req_log, response.status().as_u16(), 0);
143 }
144 anyhow::bail!("Request failed: {}", response.status());
145 }
146
147 let text = response.text().await?;
148 let size = text.len();
149
150 if let Some(req_log) = request_log {
151 self.log_request_end(req_log, 200, size);
152 }
153
154 Ok(text)
155 }
156
157 pub async fn get_html(&self, url: &str) -> Result<String> {
158 debug!("Fetching HTML from: {}", url);
159
160 let response = self
161 .client
162 .get(url)
163 .header("Accept", "text/html")
164 .send()
165 .await?;
166
167 if !response.status().is_success() {
168 anyhow::bail!("Request failed: {}", response.status());
169 }
170
171 Ok(response.text().await?)
172 }
173
174 pub fn client(&self) -> &Client {
175 &self.client
176 }
177}