sms_client/http/
mod.rs

1//! SMS-API HTTP client.
2//! This can be used to interface with the HTTP API standalone if required.
3
4use crate::http::types::*;
5use crate::http::error::*;
6
7pub mod types;
8pub mod error;
9pub mod paginator;
10
11/// Take a response from the client, verify that the status code is 200,
12/// then read JSON body and ensure success is true and finally return response value.
13async fn read_http_response<T>(response: reqwest::Response) -> HttpResult<T>
14where
15    T: serde::de::DeserializeOwned
16{
17    // Verify response status code.
18    let status = response.status();
19    if !status.is_success() {
20        let error_text = response.text().await.unwrap_or_else(|_| "Unknown error!".to_string());
21        return Err(HttpError::HttpStatus {
22            status: status.as_u16(),
23            message: error_text
24        })
25    }
26
27    // Verify JSON success status.
28    let json: serde_json::Value = response.json().await?;
29    let success = json.get("success")
30        .and_then(|v| v.as_bool())
31        .unwrap_or(false);
32
33    if !success {
34        let message = json.get("error_message")
35            .and_then(|v| v.as_str())
36            .unwrap_or("Unknown API error!")
37            .to_string();
38        return Err(HttpError::ApiError { message })
39    }
40
41    // Read response field and make into expected value.
42    let response_value = json.get("response")
43        .ok_or(HttpError::MissingResponseField)?;
44
45    serde_json::from_value(response_value.clone())
46        .map_err(HttpError::JsonError)
47}
48
49/// Read a modem-specific response that contains a "type" field and "data" field.
50/// Verifies the type matches the expected type before returning the data.
51async fn read_modem_response<T>(expected: &str, response: reqwest::Response) -> HttpResult<T>
52where
53    T: serde::de::DeserializeOwned
54{
55
56    // Verify expected response type.
57    let json_response: serde_json::Value = read_http_response(response).await?;
58    let actual = json_response.get("type")
59        .and_then(|v| v.as_str())
60        .ok_or(HttpError::MissingTypeField)?;
61
62    if actual != expected {
63        return Err(HttpError::ResponseTypeMismatch {
64            expected: expected.to_string(),
65            actual: actual.to_string()
66        });
67    }
68
69    // Extract and return the data field.
70    let data = json_response.get("data")
71        .ok_or(HttpError::MissingDataField)?;
72
73    serde_json::from_value(data.clone())
74        .map_err(HttpError::JsonError)
75}
76
77/// SMS-API HTTP interface client.
78#[derive(Debug)]
79pub struct HttpClient {
80    base_url: reqwest::Url,
81    authorization: Option<String>,
82    modem_timeout: Option<std::time::Duration>,
83    client: reqwest::Client
84}
85impl HttpClient {
86
87    /// Create a new HTTP client that uses the base_url.
88    pub fn new(config: crate::config::HttpConfig) -> HttpResult<Self> {
89        let client = reqwest::Client::builder()
90            .timeout(config.base_timeout)
91            .build()?;
92
93        Ok(Self {
94            base_url: reqwest::Url::parse(config.url.as_str())?,
95            authorization: config.authorization.map(|a| a.into()),
96            modem_timeout: config.modem_timeout,
97            client
98        })
99    }
100
101    /// Set/Remove the friendly name for a given phone number.
102    pub async fn set_friendly_name(&self, phone_number: impl Into<String>, friendly_name: Option<impl Into<String>>) -> HttpResult<bool> {
103        let body = serde_json::json!({
104            "phone_number": phone_number.into(),
105            "friendly_name": friendly_name.map(Into::into)
106        });
107
108        let url = self.base_url.join("/db/friendly-names/set")?;
109        let response = self.setup_request(false, self.client.post(url))
110            .json(&body)
111            .send()
112            .await?;
113
114        read_http_response(response).await
115    }
116
117    /// Get the friendly name associated with a given phone number.
118    pub async fn get_friendly_name(&self, phone_number: impl Into<String>) -> HttpResult<Option<String>> {
119        let body = serde_json::json!({
120            "phone_number": phone_number.into()
121        });
122
123        let url = self.base_url.join("/db/friendly-names/get")?;
124        let response = self.setup_request(false, self.client.post(url))
125            .json(&body)
126            .send()
127            .await?;
128
129        read_http_response(response).await
130    }
131
132    /// Get messages sent to and from a given phone number.
133    /// Pagination options are supported.
134    pub async fn get_messages(&self, phone_number: impl Into<String>, pagination: Option<HttpPaginationOptions>) -> HttpResult<Vec<crate::types::SmsStoredMessage>> {
135        let mut body = serde_json::json!({
136            "phone_number": phone_number.into()
137        });
138        if let Some(pagination) = pagination {
139            pagination.add_to_body(&mut body);
140        }
141
142        let url = self.base_url.join("/db/sms")?;
143        let response = self.setup_request(false, self.client.post(url))
144            .json(&body)
145            .send()
146            .await?;
147
148        read_http_response(response).await
149    }
150
151    /// Get the latest phone numbers that have been in contact with the SMS-API.
152    /// This includes both senders and receivers. Pagination options are supported.
153    pub async fn get_latest_numbers(&self, pagination: Option<HttpPaginationOptions>) -> HttpResult<Vec<LatestNumberFriendlyNamePair>> {
154        let url = self.base_url.join("/db/latest-numbers")?;
155        let mut request = self.setup_request(false, self.client.post(url));
156
157        // Only add a JSON body if there are pagination options.
158        if let Some(pagination) = pagination {
159            request = request.json(&pagination);
160        }
161
162        let response = request.send().await?;
163        read_http_response(response).await
164    }
165
166    /// Get received delivery reports for a given message_id (comes from send_sms etc).
167    /// Pagination options are supported.
168    pub async fn get_delivery_reports(&self, message_id: i64, pagination: Option<HttpPaginationOptions>) -> HttpResult<Vec<HttpSmsDeliveryReport>> {
169        let mut body = serde_json::json!({
170            "message_id": message_id
171        });
172        if let Some(pagination) = pagination {
173            pagination.add_to_body(&mut body);
174        }
175
176        let url = self.base_url.join("/db/delivery-reports")?;
177        let response = self.setup_request(false, self.client.post(url))
178            .json(&body)
179            .send()
180            .await?;
181
182        read_http_response(response).await
183    }
184
185    /// Send an SMS message to a target phone_number. The result will contain the
186    /// message reference (provided from modem) and message id (used internally).
187    /// This will use the message timeout for the request if one is set.
188    pub async fn send_sms(&self, message: &HttpOutgoingSmsMessage) -> HttpResult<HttpSmsSendResponse> {
189        let url = self.base_url.join("/sms/send")?;
190
191        // Create request, applying request timeout if one is set (+ 5).
192        // The timeout is enforced by the server, so the additional buffer is to allow for slow networking.
193        let mut request = self.setup_request(true, self.client.post(url));
194        if let Some(timeout) = message.timeout {
195            request = request.timeout(std::time::Duration::from_secs(timeout as u64 + 5));
196        }
197
198        let response = request.json(message)
199            .send()
200            .await?;
201
202        read_http_response(response).await
203    }
204
205    /// Get the carrier network status.
206    pub async fn get_network_status(&self) -> HttpResult<HttpModemNetworkStatusResponse> {
207        self.modem_request("modem-status", "NetworkStatus").await
208    }
209
210    /// Get the modem signal strength for the connected tower.
211    pub async fn get_signal_strength(&self) -> HttpResult<HttpModemSignalStrengthResponse> {
212        self.modem_request("signal-strength", "SignalStrength").await
213    }
214
215    /// Get the underlying network operator, this is often the same across
216    /// multiple service providers for a given region. Eg: vodafone.
217    pub async fn get_network_operator(&self) -> HttpResult<HttpModemNetworkOperatorResponse> {
218        self.modem_request("network-operator", "NetworkOperator").await
219    }
220
221    /// Get the SIM service provider, this is the brand that manages the contract.
222    /// This matters less than the network operator, as they're just resellers. Eg: ASDA Mobile.
223    pub async fn get_service_provider(&self) -> HttpResult<String> {
224        self.modem_request("service-provider", "ServiceProvider").await
225    }
226
227    /// Get the Modem Hat's battery level, which is used for GNSS warm starts.
228    pub async fn get_battery_level(&self) -> HttpResult<HttpModemBatteryLevelResponse> {
229        self.modem_request("battery-level", "BatteryLevel").await
230    }
231
232    /// Get device info summary result. This is a more efficient way to request all device info.
233    pub async fn get_device_info(&self) -> HttpResult<HttpSmsDeviceInfoData> {
234        let url = self.base_url.join("/sms/device-info")?;
235        let response = self.setup_request(true, self.client.get(url))
236            .send()
237            .await?;
238
239        let response = read_http_response::<HttpSmsDeviceInfoResponse>(response).await?;
240        Ok(HttpSmsDeviceInfoData::from(response))
241    }
242
243    /// Get the configured sender SMS number. This should be used primarily for client identification.
244    /// This is optional, as the API could have left this un-configured without any value set.
245    pub async fn get_phone_number(&self) -> HttpResult<Option<String>> {
246        let url = self.base_url.join("/sys/phone-number")?;
247        let response = self.setup_request(false, self.client.get(url))
248            .send()
249            .await?;
250
251        read_http_response(response).await
252    }
253
254    /// Get the modem SMS-API version string. This will be a semver format,
255    /// often with feature names added as a suffix, eg: "0.0.1+sentry".
256    pub async fn get_version(&self) -> HttpResult<String> {
257        let url = self.base_url.join("/sys/version")?;
258        let response = self.setup_request(false, self.client.get(url))
259            .send()
260            .await?;
261
262        read_http_response(response).await
263    }
264
265    /// Send an SMS modem request, the response contains a named type which is verified.
266    async fn modem_request<T>(&self, route: &str, expected: &str) -> HttpResult<T>
267    where
268        T: serde::de::DeserializeOwned
269    {
270        let url = self.base_url.join(&format!("/sms/{route}"))?;
271        let response = self.setup_request(true, self.client.get(url))
272            .send()
273            .await?;
274
275        read_modem_response::<T>(expected, response).await
276    }
277
278    /// Allow for a different timeout to be used for modem requests,
279    /// and apply optional authorization header to request builder.
280    fn setup_request(&self, is_modem: bool, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
281        let builder = if is_modem && let Some(timeout) = &self.modem_timeout {
282            builder.timeout(*timeout)
283        } else {
284            builder
285        };
286        if let Some(auth) = &self.authorization {
287            builder.header("authorization", auth)
288        } else {
289            builder
290        }
291    }
292}