esteria_api_client/
esteria.rs

1use chrono::{DateTime, Utc};
2use reqwest::Client;
3use std::collections::HashMap;
4use thiserror::Error;
5
6/// Error types for SMS operations
7#[derive(Error, Debug)]
8pub enum SmsError {
9    #[error("SMS sending failed to: {number}, {message}")]
10    SendFailed { number: String, message: String },
11    #[error("HTTP request failed: {0}")]
12    RequestFailed(#[from] reqwest::Error),
13}
14
15bitflags::bitflags! {
16    /// Flags for SMS sending options
17    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18    pub struct SmsFlags: u32 {
19        /// Enable debug mode
20        const DEBUG   = 0b0000_0001;
21        /// Disable logging
22        const NOLOG   = 0b0000_0010;
23        /// Send as flash SMS
24        const FLASH   = 0b0000_0100;
25        /// Test mode (don't actually send)
26        const TEST    = 0b0000_1000;
27        /// No blacklist check
28        const NOBL    = 0b0001_0000;
29        /// Convert characters
30        const CONVERT = 0b0010_0000;
31    }
32}
33
34/// SMS encoding options
35#[derive(Debug, Clone, Copy)]
36pub enum Encoding {
37    /// Default encoding
38    Default,
39    /// 8-bit encoding
40    EightBit,
41    /// User Data Header encoding
42    Udh,
43}
44
45/// SMS API client for Esteria
46pub struct SmsClient {
47    api_base_url: String,
48    client: Client,
49}
50
51/// Request structure for sending SMS
52pub struct SmsRequest<'a> {
53    pub api_key: &'a str,
54    pub sender: &'a str,
55    pub number: &'a str,
56    pub text: &'a str,
57    pub time: Option<DateTime<Utc>>,
58    pub dlr_url: Option<&'a str>,
59    pub expired: Option<i32>,
60    pub flags: SmsFlags,
61    pub user_key: Option<&'a str>,
62    pub encoding: Encoding,
63}
64
65impl<'a> SmsRequest<'a> {
66    /// Create a new SMS request with required parameters
67    #[must_use]
68    pub fn new(api_key: &'a str, sender: &'a str, number: &'a str, text: &'a str) -> Self {
69        Self {
70            api_key,
71            sender,
72            number,
73            text,
74            time: None,
75            dlr_url: None,
76            expired: None,
77            flags: SmsFlags::empty(),
78            user_key: None,
79            encoding: Encoding::EightBit,
80        }
81    }
82
83    /// Set scheduled delivery time
84    #[must_use]
85    pub fn with_time(mut self, time: DateTime<Utc>) -> Self {
86        self.time = Some(time);
87        self
88    }
89
90    /// Set delivery report URL
91    #[must_use]
92    pub fn with_dlr_url(mut self, dlr_url: &'a str) -> Self {
93        self.dlr_url = Some(dlr_url);
94        self
95    }
96
97    /// Set expiration time in minutes
98    #[must_use]
99    pub fn with_expired(mut self, expired: i32) -> Self {
100        self.expired = Some(expired);
101        self
102    }
103
104    /// Set SMS flags
105    #[must_use]
106    pub fn with_flags(mut self, flags: SmsFlags) -> Self {
107        self.flags = flags;
108        self
109    }
110
111    /// Set user key for tracking
112    #[must_use]
113    pub fn with_user_key(mut self, user_key: &'a str) -> Self {
114        self.user_key = Some(user_key);
115        self
116    }
117
118    /// Set encoding
119    #[must_use]
120    pub fn with_encoding(mut self, encoding: Encoding) -> Self {
121        self.encoding = encoding;
122        self
123    }
124}
125
126impl SmsClient {
127    /// Create a new SMS client with the given API base URL
128    #[must_use]
129    pub fn new(api_base_url: String) -> Self {
130        Self {
131            api_base_url,
132            client: Client::new(),
133        }
134    }
135
136    /// Send an SMS message
137    ///
138    /// Returns the message ID on success (> 100)
139    ///
140    /// # Errors
141    ///
142    /// Returns `SmsError::SendFailed` if the API returns an error code (< 100)
143    /// or `SmsError::RequestFailed` if the HTTP request fails
144    pub async fn send_sms(&self, request: SmsRequest<'_>) -> Result<i32, SmsError> {
145        let mut params: HashMap<&str, String> = HashMap::new();
146
147        params.insert("api-key", request.api_key.to_string());
148        params.insert("sender", request.sender.to_string());
149        params.insert("number", request.number.trim_start_matches('+').to_string());
150        params.insert("text", request.text.to_string());
151
152        if let Some(time) = request.time {
153            params.insert("time", time.format("%Y-%m-%dT%H:%M:%S").to_string());
154        }
155
156        if let Some(dlr_url) = request.dlr_url {
157            params.insert("dlr-url", dlr_url.to_string());
158        }
159
160        if let Some(expired) = request.expired {
161            params.insert("expired", expired.to_string());
162        }
163
164        if request.flags.contains(SmsFlags::DEBUG) {
165            params.insert("flag-debug", "1".to_string());
166        }
167
168        if request.flags.contains(SmsFlags::NOLOG) {
169            params.insert("flag-nolog", "3".to_string());
170        }
171
172        if request.flags.contains(SmsFlags::FLASH) {
173            params.insert("flag-flash", "1".to_string());
174        }
175
176        if request.flags.contains(SmsFlags::TEST) {
177            params.insert("flag-test", "1".to_string());
178        }
179
180        if request.flags.contains(SmsFlags::NOBL) {
181            params.insert("flag-nobl", "1".to_string());
182        }
183
184        if request.flags.contains(SmsFlags::CONVERT) {
185            params.insert("flag-convert", "1".to_string());
186        }
187
188        if let Some(user_key) = request.user_key {
189            params.insert("user-key", user_key.to_string());
190        }
191
192        match request.encoding {
193            Encoding::Udh => {
194                params.insert("udh", "1".to_string());
195                params.insert("coding", "1".to_string());
196            }
197            Encoding::EightBit => {
198                params.insert("coding", "1".to_string());
199            }
200            Encoding::Default => {}
201        }
202
203        let url = format!("{}/send", self.api_base_url);
204        let response = self.client.get(&url).query(&params).send().await?;
205
206        let resp_text = response.text().await?;
207
208        let result = resp_text.trim().parse::<i32>().ok();
209
210        if let Some(code) = result {
211            if code > 100 {
212                return Ok(code);
213            }
214
215            let error_msg = get_response_code_message(code);
216            log::error!("SMS sending failed to: {}, {}", request.number, error_msg);
217
218            return Err(SmsError::SendFailed {
219                number: request.number.to_string(),
220                message: error_msg.to_string(),
221            });
222        }
223
224        log::error!("SMS sending failed to: {}, unknown error", request.number);
225        Err(SmsError::SendFailed {
226            number: request.number.to_string(),
227            message: "unknown error".to_string(),
228        })
229    }
230}
231
232fn get_response_code_message(code: i32) -> &'static str {
233    match code {
234        1 => "system internal error",
235        2 => "missing PARAM_NAME parameter",
236        3 => "unable to authenticate",
237        4 => "IP ADDRESS is not allowed",
238        5 => "invalid SENDER parameter",
239        6 => "SENDER is not allowed",
240        7 => "invalid NUMBER parameter",
241        8 => "invalid CODING parameter",
242        9 => "unable to convert TEXT",
243        10 => "length of UDH and TEXT too long",
244        11 => "empty TEXT parameter",
245        12 => "invalid TIME parameter",
246        13 => "invalid EXPIRED parameter",
247        14 => "invalid DLR-URL parameter",
248        15 => "Invalid FLAG-FLASH parameter",
249        16 => "invalid FLAG-NOLOG parameter",
250        17 => "invalid FLAG-TEST parameter",
251        18 => "invalid FLAG-NOBL parameter",
252        19 => "invalid FLAG-CONVERT parameter",
253        _ => "unknown error",
254    }
255}