1use crate::http::error::{HttpError, HttpResult};
5use crate::http::types::{
6 HttpModemBatteryLevelResponse, HttpModemNetworkOperatorResponse,
7 HttpModemNetworkStatusResponse, HttpModemSignalStrengthResponse, HttpOutgoingSmsMessage,
8 HttpPaginationOptions, HttpSmsDeliveryReport, HttpSmsDeviceInfoData, HttpSmsDeviceInfoResponse,
9 HttpSmsSendResponse, LatestNumberFriendlyNamePair,
10};
11
12pub mod error;
13pub mod paginator;
14pub mod types;
15
16async fn read_http_response<T>(response: reqwest::Response) -> HttpResult<T>
19where
20 T: serde::de::DeserializeOwned,
21{
22 let is_json = response
23 .headers()
24 .get(reqwest::header::CONTENT_TYPE)
25 .and_then(|ct| ct.to_str().ok())
26 .map(|ct| ct.contains("application/json"))
27 .unwrap_or(false);
28
29 if is_json {
30 let json: serde_json::Value = response.json().await?;
32 let success = json
33 .get("success")
34 .and_then(serde_json::Value::as_bool)
35 .unwrap_or(false);
36
37 if !success {
38 let message = json
39 .get("error")
40 .and_then(|v| v.as_str())
41 .unwrap_or("Unknown API error!")
42 .to_string();
43
44 return Err(HttpError::ApiError(message));
45 }
46
47 let response_value = json
49 .get("response")
50 .ok_or(HttpError::MissingResponseField)?;
51
52 return serde_json::from_value(response_value.clone()).map_err(HttpError::JsonError);
53 }
54
55 let status = response.status();
57 if !status.is_success() {
58 let error_text = response
59 .text()
60 .await
61 .unwrap_or_else(|_| "Unknown error!".to_string());
62
63 return Err(HttpError::HttpStatus {
64 status: status.as_u16(),
65 message: error_text,
66 });
67 }
68
69 Err(HttpError::MissingResponseField)
70}
71
72async fn read_modem_response<T>(expected: &str, response: reqwest::Response) -> HttpResult<T>
75where
76 T: serde::de::DeserializeOwned,
77{
78 let json_response: serde_json::Value = read_http_response(response).await?;
80 let actual = json_response
81 .get("type")
82 .and_then(|v| v.as_str())
83 .ok_or(HttpError::MissingTypeField)?;
84
85 if actual != expected {
86 return Err(HttpError::ResponseTypeMismatch {
87 expected: expected.to_string(),
88 actual: actual.to_string(),
89 });
90 }
91
92 let data = json_response
94 .get("data")
95 .ok_or(HttpError::MissingDataField)?;
96
97 serde_json::from_value(data.clone()).map_err(HttpError::JsonError)
98}
99
100fn client_builder(config: Option<&crate::config::TLSConfig>) -> HttpResult<reqwest::ClientBuilder> {
102 let builder = reqwest::Client::builder();
103 let Some(tls_config) = config.as_ref() else {
104 return Ok(builder);
105 };
106
107 #[cfg(not(any(feature = "http-tls-rustls", feature = "http-tls-native")))]
108 {
109 let _ = tls_config; Err(HttpError::TLSError(
111 "TLS configuration provided but no TLS features enabled. Enable either 'http-tls-rustls' or 'http-tls-native' feature".to_string()
112 ))
113 }
114
115 #[cfg(any(feature = "http-tls-rustls", feature = "http-tls-native"))]
116 {
117 let mut builder = builder;
118
119 #[cfg(feature = "http-tls-rustls")]
121 {
122 builder = builder.use_rustls_tls();
123 }
124
125 #[cfg(feature = "http-tls-native")]
126 {
127 builder = builder.use_native_tls();
128 }
129
130 let certificate = load_certificate(&tls_config.certificate)?;
132 Ok(builder.add_root_certificate(certificate))
133 }
134}
135
136#[cfg(any(feature = "http-tls-rustls", feature = "http-tls-native"))]
138fn load_certificate(cert_path: &std::path::Path) -> HttpResult<reqwest::tls::Certificate> {
139 let cert_data = std::fs::read(cert_path).map_err(HttpError::IOError)?;
140
141 if let Some(ext) = cert_path.extension().and_then(|s| s.to_str()) {
143 match ext {
144 "pem" => return Ok(reqwest::tls::Certificate::from_pem(&cert_data)?),
145 "der" => return Ok(reqwest::tls::Certificate::from_der(&cert_data)?),
146 "crt" => {
147 if cert_data.starts_with(b"-----BEGIN") {
148 return Ok(reqwest::tls::Certificate::from_pem(&cert_data)?);
149 } else {
150 return Ok(reqwest::tls::Certificate::from_der(&cert_data)?);
151 }
152 }
153 _ => {} }
155 }
156
157 reqwest::tls::Certificate::from_pem(&cert_data)
159 .or_else(|_| reqwest::tls::Certificate::from_der(&cert_data))
160 .map_err(Into::into)
161}
162
163#[derive(Debug)]
165pub struct HttpClient {
166 base_url: reqwest::Url,
167 authorization: Option<String>,
168 modem_timeout: Option<std::time::Duration>,
169 client: reqwest::Client,
170}
171impl HttpClient {
172 pub fn new(
174 config: crate::config::HttpConfig,
175 tls: Option<&crate::config::TLSConfig>,
176 ) -> HttpResult<Self> {
177 let client = client_builder(tls)?.timeout(config.base_timeout).build()?;
178
179 Ok(Self {
180 base_url: reqwest::Url::parse(config.url.as_str())?,
181 authorization: config.authorization,
182 modem_timeout: config.modem_timeout,
183 client,
184 })
185 }
186
187 pub async fn set_friendly_name(
189 &self,
190 phone_number: impl Into<String>,
191 friendly_name: Option<impl Into<String>>,
192 ) -> HttpResult<bool> {
193 let body = serde_json::json!({
194 "phone_number": phone_number.into(),
195 "friendly_name": friendly_name.map(Into::into)
196 });
197
198 let url = self.base_url.join("/db/friendly-names/set")?;
199 let response = self
200 .setup_request(false, self.client.post(url))
201 .json(&body)
202 .send()
203 .await?;
204
205 read_http_response(response).await
206 }
207
208 pub async fn get_friendly_name(
210 &self,
211 phone_number: impl Into<String>,
212 ) -> HttpResult<Option<String>> {
213 let body = serde_json::json!({
214 "phone_number": phone_number.into()
215 });
216
217 let url = self.base_url.join("/db/friendly-names/get")?;
218 let response = self
219 .setup_request(false, self.client.post(url))
220 .json(&body)
221 .send()
222 .await?;
223
224 read_http_response(response).await
225 }
226
227 pub async fn get_messages(
230 &self,
231 phone_number: impl Into<String>,
232 pagination: Option<HttpPaginationOptions>,
233 ) -> HttpResult<Vec<crate::types::SmsStoredMessage>> {
234 let mut body = serde_json::json!({
235 "phone_number": phone_number.into()
236 });
237 if let Some(pagination) = pagination {
238 pagination.add_to_body(&mut body);
239 }
240
241 let url = self.base_url.join("/db/sms")?;
242 let response = self
243 .setup_request(false, self.client.post(url))
244 .json(&body)
245 .send()
246 .await?;
247
248 read_http_response(response).await
249 }
250
251 pub async fn get_latest_numbers(
254 &self,
255 pagination: Option<HttpPaginationOptions>,
256 ) -> HttpResult<Vec<LatestNumberFriendlyNamePair>> {
257 let url = self.base_url.join("/db/latest-numbers")?;
258 let mut request = self.setup_request(false, self.client.post(url));
259
260 if let Some(pagination) = pagination {
262 request = request.json(&pagination);
263 }
264
265 let response = request.send().await?;
266 read_http_response(response).await
267 }
268
269 pub async fn get_delivery_reports(
272 &self,
273 message_id: i64,
274 pagination: Option<HttpPaginationOptions>,
275 ) -> HttpResult<Vec<HttpSmsDeliveryReport>> {
276 let mut body = serde_json::json!({
277 "message_id": message_id
278 });
279 if let Some(pagination) = pagination {
280 pagination.add_to_body(&mut body);
281 }
282
283 let url = self.base_url.join("/db/delivery-reports")?;
284 let response = self
285 .setup_request(false, self.client.post(url))
286 .json(&body)
287 .send()
288 .await?;
289
290 read_http_response(response).await
291 }
292
293 pub async fn send_sms(
297 &self,
298 message: &HttpOutgoingSmsMessage,
299 ) -> HttpResult<HttpSmsSendResponse> {
300 let url = self.base_url.join("/sms/send")?;
301
302 let mut request = self.setup_request(true, self.client.post(url));
305 if let Some(timeout) = message.timeout {
306 request = request.timeout(std::time::Duration::from_secs(u64::from(timeout) + 5));
307 }
308
309 let response = request.json(message).send().await?;
310
311 read_http_response(response).await
312 }
313
314 pub async fn get_network_status(&self) -> HttpResult<HttpModemNetworkStatusResponse> {
316 self.modem_request("modem-status", "NetworkStatus").await
317 }
318
319 pub async fn get_signal_strength(&self) -> HttpResult<HttpModemSignalStrengthResponse> {
321 self.modem_request("signal-strength", "SignalStrength")
322 .await
323 }
324
325 pub async fn get_network_operator(&self) -> HttpResult<HttpModemNetworkOperatorResponse> {
328 self.modem_request("network-operator", "NetworkOperator")
329 .await
330 }
331
332 pub async fn get_service_provider(&self) -> HttpResult<String> {
335 self.modem_request("service-provider", "ServiceProvider")
336 .await
337 }
338
339 pub async fn get_battery_level(&self) -> HttpResult<HttpModemBatteryLevelResponse> {
341 self.modem_request("battery-level", "BatteryLevel").await
342 }
343
344 pub async fn get_device_info(&self) -> HttpResult<HttpSmsDeviceInfoData> {
346 let url = self.base_url.join("/sms/device-info")?;
347 let response = self
348 .setup_request(true, self.client.get(url))
349 .send()
350 .await?;
351
352 let response = read_http_response::<HttpSmsDeviceInfoResponse>(response).await?;
353 Ok(HttpSmsDeviceInfoData::from(response))
354 }
355
356 pub async fn get_phone_number(&self) -> HttpResult<Option<String>> {
359 let url = self.base_url.join("/sys/phone-number")?;
360 let response = self
361 .setup_request(false, self.client.get(url))
362 .send()
363 .await?;
364
365 read_http_response(response).await
366 }
367
368 pub async fn get_version(&self) -> HttpResult<String> {
371 let url = self.base_url.join("/sys/version")?;
372 let response = self
373 .setup_request(false, self.client.get(url))
374 .send()
375 .await?;
376
377 read_http_response(response).await
378 }
379
380 async fn modem_request<T>(&self, route: &str, expected: &str) -> HttpResult<T>
382 where
383 T: serde::de::DeserializeOwned,
384 {
385 let url = self.base_url.join(&format!("/sms/{route}"))?;
386 let response = self
387 .setup_request(true, self.client.get(url))
388 .send()
389 .await?;
390
391 read_modem_response::<T>(expected, response).await
392 }
393
394 fn setup_request(
397 &self,
398 is_modem: bool,
399 builder: reqwest::RequestBuilder,
400 ) -> reqwest::RequestBuilder {
401 let builder = if is_modem && let Some(timeout) = &self.modem_timeout {
402 builder.timeout(*timeout)
403 } else {
404 builder
405 };
406 if let Some(auth) = &self.authorization {
407 builder.header("authorization", auth)
408 } else {
409 builder
410 }
411 }
412}