use crate::http::error::{HttpError, HttpResult};
use sms_types::gnss::{FixStatus, PositionReport};
use sms_types::http::{
HttpModemBatteryLevelResponse, HttpModemNetworkOperatorResponse,
HttpModemNetworkStatusResponse, HttpModemSignalStrengthResponse, HttpPaginationOptions,
HttpSmsDeviceInfoResponse, HttpSmsSendResponse, LatestNumberFriendlyNamePair,
};
use sms_types::sms::{SmsDeliveryReport, SmsOutgoingMessage};
pub mod error;
pub mod paginator;
async fn read_http_response<T>(response: reqwest::Response) -> HttpResult<T>
where
T: serde::de::DeserializeOwned,
{
let is_json = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.is_some_and(|ct| ct.contains("application/json"));
if is_json {
let json: serde_json::Value = response.json().await?;
let success = json
.get("success")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
if !success {
let message = json
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("Unknown API error!")
.to_string();
return Err(HttpError::ApiError(message));
}
let response_value = json
.get("response")
.ok_or(HttpError::MissingResponseField)?;
return serde_json::from_value(response_value.clone()).map_err(HttpError::JsonError);
}
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error!".to_string());
return Err(HttpError::HttpStatus {
status: status.as_u16(),
message: error_text,
});
}
Err(HttpError::MissingResponseField)
}
fn client_builder(config: Option<&crate::config::TLSConfig>) -> HttpResult<reqwest::ClientBuilder> {
let builder = reqwest::Client::builder();
let Some(tls_config) = config.as_ref() else {
return Ok(builder);
};
#[cfg(not(any(feature = "http-tls-rustls", feature = "http-tls-native")))]
{
let _ = tls_config; Err(HttpError::TLSError(
"TLS configuration provided but no TLS features enabled. Enable either 'http-tls-rustls' or 'http-tls-native' feature".to_string()
))
}
#[cfg(any(feature = "http-tls-rustls", feature = "http-tls-native"))]
{
let mut builder = builder;
#[cfg(feature = "http-tls-rustls")]
{
builder = builder.use_rustls_tls();
}
#[cfg(feature = "http-tls-native")]
{
builder = builder.use_native_tls();
}
let certificate = load_certificate(&tls_config.certificate)?;
Ok(builder.add_root_certificate(certificate))
}
}
#[cfg(any(feature = "http-tls-rustls", feature = "http-tls-native"))]
fn load_certificate(cert_path: &std::path::Path) -> HttpResult<reqwest::tls::Certificate> {
let cert_data = std::fs::read(cert_path).map_err(HttpError::IOError)?;
if let Some(ext) = cert_path.extension().and_then(|s| s.to_str()) {
match ext {
"pem" => return Ok(reqwest::tls::Certificate::from_pem(&cert_data)?),
"der" => return Ok(reqwest::tls::Certificate::from_der(&cert_data)?),
"crt" => {
if cert_data.starts_with(b"-----BEGIN") {
return Ok(reqwest::tls::Certificate::from_pem(&cert_data)?);
} else {
return Ok(reqwest::tls::Certificate::from_der(&cert_data)?);
}
}
_ => {} }
}
reqwest::tls::Certificate::from_pem(&cert_data)
.or_else(|_| reqwest::tls::Certificate::from_der(&cert_data))
.map_err(Into::into)
}
#[derive(Debug)]
pub struct HttpClient {
base_url: reqwest::Url,
authorization: Option<String>,
modem_timeout: Option<std::time::Duration>,
client: reqwest::Client,
}
impl HttpClient {
pub fn new(
config: crate::config::HttpConfig,
tls: Option<&crate::config::TLSConfig>,
) -> HttpResult<Self> {
let client = client_builder(tls)?.timeout(config.base_timeout).build()?;
Ok(Self {
base_url: reqwest::Url::parse(config.url.as_str())?,
authorization: config.authorization,
modem_timeout: config.modem_timeout,
client,
})
}
pub async fn set_friendly_name(
&self,
phone_number: impl Into<String>,
friendly_name: Option<impl Into<String>>,
) -> HttpResult<bool> {
let body = serde_json::json!({
"phone_number": phone_number.into(),
"friendly_name": friendly_name.map(Into::into)
});
let url = self.base_url.join("/db/friendly-names/set")?;
let response = self
.setup_request(false, self.client.post(url))
.json(&body)
.send()
.await?;
read_http_response(response).await
}
pub async fn get_friendly_name(
&self,
phone_number: impl Into<String>,
) -> HttpResult<Option<String>> {
let body = serde_json::json!({
"phone_number": phone_number.into()
});
let url = self.base_url.join("/db/friendly-names/get")?;
let response = self
.setup_request(false, self.client.post(url))
.json(&body)
.send()
.await?;
read_http_response(response).await
}
pub async fn get_messages(
&self,
phone_number: impl Into<String>,
pagination: Option<HttpPaginationOptions>,
) -> HttpResult<Vec<sms_types::sms::SmsMessage>> {
let mut body = serde_json::json!({
"phone_number": phone_number.into()
});
if let Some(pagination) = pagination {
pagination.add_to_body(&mut body);
}
let url = self.base_url.join("/db/messages")?;
let response = self
.setup_request(false, self.client.post(url))
.json(&body)
.send()
.await?;
read_http_response(response).await
}
pub async fn get_latest_numbers(
&self,
pagination: Option<HttpPaginationOptions>,
) -> HttpResult<Vec<LatestNumberFriendlyNamePair>> {
let url = self.base_url.join("/db/latest-numbers")?;
let mut request = self.setup_request(false, self.client.post(url));
if let Some(pagination) = pagination {
request = request.json(&pagination);
}
let response = request.send().await?;
read_http_response(response).await
}
pub async fn get_delivery_reports(
&self,
message_id: i64,
pagination: Option<HttpPaginationOptions>,
) -> HttpResult<Vec<SmsDeliveryReport>> {
let mut body = serde_json::json!({
"message_id": message_id
});
if let Some(pagination) = pagination {
pagination.add_to_body(&mut body);
}
let url = self.base_url.join("/db/delivery-reports")?;
let response = self
.setup_request(false, self.client.post(url))
.json(&body)
.send()
.await?;
read_http_response(response).await
}
pub async fn send_sms(&self, message: &SmsOutgoingMessage) -> HttpResult<HttpSmsSendResponse> {
let url = self.base_url.join("/sms/send")?;
let mut request = self.setup_request(true, self.client.post(url));
if let Some(timeout) = message.timeout {
request = request.timeout(std::time::Duration::from_secs(u64::from(timeout) + 5));
}
let response = request.json(message).send().await?;
read_http_response(response).await
}
pub async fn get_network_status(&self) -> HttpResult<HttpModemNetworkStatusResponse> {
self.modem_request("/sms/modem-status").await
}
pub async fn get_signal_strength(&self) -> HttpResult<HttpModemSignalStrengthResponse> {
self.modem_request("/sms/signal-strength").await
}
pub async fn get_network_operator(&self) -> HttpResult<HttpModemNetworkOperatorResponse> {
self.modem_request("/sms/network-operator").await
}
pub async fn get_service_provider(&self) -> HttpResult<String> {
self.modem_request("/sms/service-provider").await
}
pub async fn get_battery_level(&self) -> HttpResult<HttpModemBatteryLevelResponse> {
self.modem_request("/sms/battery-level").await
}
pub async fn get_gnss_status(&self) -> HttpResult<FixStatus> {
self.modem_request("/gnss/status").await
}
pub async fn get_gnss_location(&self) -> HttpResult<PositionReport> {
self.modem_request("/gnss/location").await
}
pub async fn get_device_info(&self) -> HttpResult<HttpSmsDeviceInfoResponse> {
let url = self.base_url.join("/sms/device-info")?;
let response = self
.setup_request(true, self.client.get(url))
.send()
.await?;
read_http_response(response).await
}
pub async fn get_phone_number(&self) -> HttpResult<Option<String>> {
let url = self.base_url.join("/sys/phone-number")?;
let response = self
.setup_request(false, self.client.get(url))
.send()
.await?;
read_http_response(response).await
}
pub async fn get_version(&self) -> HttpResult<String> {
let url = self.base_url.join("/sys/version")?;
let response = self
.setup_request(false, self.client.get(url))
.send()
.await?;
read_http_response(response).await
}
pub async fn set_log_level(&self, level: impl Into<String>) -> HttpResult<bool> {
let body = serde_json::json!({
"level": level.into()
});
let url = self.base_url.join("/sys/set-log-level")?;
let response = self
.setup_request(false, self.client.post(url))
.json(&body)
.send()
.await?;
read_http_response(response).await
}
async fn modem_request<T>(&self, route: &str) -> HttpResult<T>
where
T: serde::de::DeserializeOwned,
{
let url = self.base_url.join(route)?;
let response = self
.setup_request(true, self.client.get(url))
.send()
.await?;
read_http_response::<T>(response).await
}
fn setup_request(
&self,
is_modem: bool,
builder: reqwest::RequestBuilder,
) -> reqwest::RequestBuilder {
let builder = if is_modem && let Some(timeout) = &self.modem_timeout {
builder.timeout(*timeout)
} else {
builder
};
if let Some(auth) = &self.authorization {
builder.header("authorization", auth)
} else {
builder
}
}
}