1use base64::Engine;
2use chrono::{DateTime, Utc};
3use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
4use reqwest::Method;
5
6use crate::config::ClientConfig;
7use crate::http::HttpTransport;
8use crate::types::*;
9use crate::Error;
10
11pub struct Client {
45 transport: HttpTransport,
46}
47
48impl Client {
49 pub fn new(config: ClientConfig) -> Result<Self, Error> {
53 config.validate()?;
54
55 let auth_header = match &config.token {
56 Some(token) => format!("Bearer {}", token),
57 None => {
58 let credentials = format!(
59 "{}:{}",
60 config.username.as_deref().unwrap_or(""),
61 config.password.as_deref().unwrap_or("")
62 );
63 let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
64 format!("Basic {}", encoded)
65 }
66 };
67
68 let http_client = config.http_client.map(Ok).unwrap_or_else(|| {
69 reqwest::Client::builder()
70 .timeout(std::time::Duration::from_secs(30))
71 .build()
72 })?;
73 let user_agent = format!("android-sms-gateway/{} (client; rust)", crate::VERSION);
74
75 Ok(Self {
76 transport: HttpTransport::new(http_client, config.base_url, auth_header, user_agent),
77 })
78 }
79
80 pub async fn check_health(&self) -> Result<HealthResponse, Error> {
82 self.transport
83 .request_json::<(), HealthResponse>(Method::GET, "/health", None)
84 .await
85 }
86
87 pub async fn send(
92 &self,
93 message: &Message,
94 options: &SendOptions,
95 ) -> Result<MessageState, Error> {
96 message.validate()?;
97 let query = options.to_url_query();
98 let path = build_path("/messages", &query);
99
100 self.transport
101 .request_json(Method::POST, &path, Some(message))
102 .await
103 }
104
105 pub async fn list_messages(
112 &self,
113 options: &ListMessagesOptions,
114 ) -> Result<(Vec<MessageState>, Option<u64>), Error> {
115 options.validate()?;
116 let query = options.to_url_query();
117 let path = build_path("/messages", &query);
118
119 let (results, headers): (Vec<MessageState>, _) = self
120 .transport
121 .request_json_with_headers(Method::GET, &path, None::<&()>)
122 .await?;
123
124 let total = headers
125 .get("X-Total-Count")
126 .and_then(|v| v.to_str().ok())
127 .and_then(|v| v.parse::<u64>().ok());
128
129 Ok((results, total))
130 }
131
132 pub async fn get_message_state(&self, id: &str) -> Result<MessageState, Error> {
134 let path = format!("/messages/{}", encode_path_segment(id));
135 self.transport
136 .request_json::<(), MessageState>(Method::GET, &path, None)
137 .await
138 }
139
140 pub async fn cancel_message(&self, id: &str) -> Result<(), Error> {
142 let path = format!("/messages/{}", encode_path_segment(id));
143 self.transport
144 .request_empty::<()>(Method::DELETE, &path, None)
145 .await
146 }
147
148 pub async fn list_devices(&self) -> Result<Vec<Device>, Error> {
150 self.transport
151 .request_json::<(), Vec<Device>>(Method::GET, "/devices", None)
152 .await
153 }
154
155 pub async fn delete_device(&self, id: &str) -> Result<(), Error> {
157 let path = format!("/devices/{}", encode_path_segment(id));
158 self.transport
159 .request_empty::<()>(Method::DELETE, &path, None)
160 .await
161 }
162
163 pub async fn get_settings(&self) -> Result<DeviceSettings, Error> {
165 self.transport
166 .request_json::<(), DeviceSettings>(Method::GET, "/settings", None)
167 .await
168 }
169
170 pub async fn replace_settings(
172 &self,
173 settings: &DeviceSettings,
174 ) -> Result<DeviceSettings, Error> {
175 settings.validate()?;
176 self.transport
177 .request_json(Method::PUT, "/settings", Some(settings))
178 .await
179 }
180
181 pub async fn update_settings(
183 &self,
184 settings: &DeviceSettings,
185 ) -> Result<DeviceSettings, Error> {
186 settings.validate()?;
187 self.transport
188 .request_json(Method::PATCH, "/settings", Some(settings))
189 .await
190 }
191
192 pub async fn list_webhooks(&self) -> Result<Vec<Webhook>, Error> {
194 self.transport
195 .request_json::<(), Vec<Webhook>>(Method::GET, "/webhooks", None)
196 .await
197 }
198
199 pub async fn register_webhook(&self, webhook: &Webhook) -> Result<Webhook, Error> {
201 webhook.validate()?;
202 self.transport
203 .request_json(Method::POST, "/webhooks", Some(webhook))
204 .await
205 }
206
207 pub async fn delete_webhook(&self, id: &str) -> Result<(), Error> {
209 let path = format!("/webhooks/{}", encode_path_segment(id));
210 self.transport
211 .request_empty::<()>(Method::DELETE, &path, None)
212 .await
213 }
214
215 pub async fn generate_token(&self, request: &TokenRequest) -> Result<TokenResponse, Error> {
217 self.transport
218 .request_json(Method::POST, "/auth/token", Some(request))
219 .await
220 }
221
222 pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenResponse, Error> {
226 let auth_header = format!("Bearer {}", refresh_token);
227 self.transport
228 .request_json_custom_auth::<(), TokenResponse>(
229 Method::POST,
230 "/auth/token/refresh",
231 None,
232 &auth_header,
233 )
234 .await
235 }
236
237 pub async fn revoke_token(&self, jti: &str) -> Result<(), Error> {
239 let path = format!("/auth/token/{}", encode_path_segment(jti));
240 self.transport
241 .request_empty::<()>(Method::DELETE, &path, None)
242 .await
243 }
244
245 pub async fn refresh_inbox(&self, request: &InboxRefreshRequest) -> Result<(), Error> {
247 self.transport
248 .request_empty(Method::POST, "/inbox/refresh", Some(request))
249 .await
250 }
251
252 pub async fn list_inbox_messages(
258 &self,
259 options: &ListInboxOptions,
260 ) -> Result<(Vec<IncomingMessage>, Option<u64>), Error> {
261 options.validate()?;
262 let query = options.to_url_query();
263 let path = build_path("/inbox", &query);
264
265 let (results, headers): (Vec<IncomingMessage>, _) = self
266 .transport
267 .request_json_with_headers(Method::GET, &path, None::<&()>)
268 .await?;
269
270 let total = headers
271 .get("X-Total-Count")
272 .and_then(|v| v.to_str().ok())
273 .and_then(|v| v.parse::<u64>().ok());
274
275 Ok((results, total))
276 }
277
278 pub async fn get_logs(
280 &self,
281 from: &DateTime<Utc>,
282 to: &DateTime<Utc>,
283 ) -> Result<Vec<LogEntry>, Error> {
284 if from > to {
285 return Err(Error::Validation(
286 "`from` date must be before `to` date".to_string(),
287 ));
288 }
289 let path = format!(
290 "/logs?from={}&to={}",
291 encode_path_segment(&from.to_rfc3339()),
292 encode_path_segment(&to.to_rfc3339())
293 );
294 self.transport
295 .request_json::<(), Vec<LogEntry>>(Method::GET, &path, None)
296 .await
297 }
298
299 pub async fn export_inbox(&self, request: &MessagesExportRequest) -> Result<(), Error> {
301 self.transport
302 .request_empty(Method::POST, "/inbox/export", Some(request))
303 .await
304 }
305
306 pub async fn download_attachment(
310 &self,
311 message_id: &str,
312 part_id: i32,
313 ) -> Result<Vec<u8>, Error> {
314 let path = format!(
315 "/inbox/{}/attachments/{}",
316 encode_path_segment(message_id),
317 part_id
318 );
319 self.transport
320 .request_bytes::<()>(Method::GET, &path, None)
321 .await
322 }
323}
324
325fn encode_path_segment(s: &str) -> String {
326 const PATH_SEGMENT: &AsciiSet = &NON_ALPHANUMERIC
327 .remove(b'-')
328 .remove(b'.')
329 .remove(b'_')
330 .remove(b'~');
331 utf8_percent_encode(s, PATH_SEGMENT).to_string()
332}
333
334fn build_path(base: &str, query: &str) -> String {
335 if query.is_empty() {
336 base.to_string()
337 } else {
338 format!("{}?{}", base, query)
339 }
340}