smsir_rust/
service.rs

1#[derive(Clone, Debug)]
2pub struct SmsIrServiceBuilder {
3    api_key: String,
4    base_uri: String,
5    timeout: std::time::Duration,
6    proxy: Option<String>,
7    retry_count: usize,
8    retry_delay: std::time::Duration,
9    log_level: LogLevel,
10}
11
12impl SmsIrServiceBuilder {
13    pub fn new(api_key: impl Into<String>) -> Self {
14        Self {
15            api_key: api_key.into(),
16            base_uri: "https://api.sms.ir/v1/".to_string(),
17            timeout: std::time::Duration::from_secs(30),
18            proxy: None,
19            retry_count: 3,
20            retry_delay: std::time::Duration::from_millis(500),
21            log_level: LogLevel::Info,
22        }
23    }
24    pub fn base_uri(mut self, uri: &str) -> Self {
25        self.base_uri = uri.to_string(); self
26    }
27    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
28        self.timeout = timeout; self
29    }
30    pub fn proxy(mut self, proxy: &str) -> Self {
31        self.proxy = Some(proxy.to_string()); self
32    }
33    pub fn retry_count(mut self, count: usize) -> Self {
34        self.retry_count = count; self
35    }
36    pub fn retry_delay(mut self, delay: std::time::Duration) -> Self {
37        self.retry_delay = delay; self
38    }
39    pub fn log_level(mut self, level: LogLevel) -> Self {
40        self.log_level = level; self
41    }
42    pub fn build(self) -> Result<SmsIrService, SmsIrError> {
43        let mut client_builder = reqwest::Client::builder();
44        if let Some(proxy_url) = &self.proxy {
45            client_builder = client_builder.proxy(reqwest::Proxy::all(proxy_url).expect("Invalid proxy"));
46        }
47        let client = client_builder.build().expect("Failed to build client");
48        Ok(SmsIrService {
49            api_key: self.api_key,
50            base_uri: self.base_uri,
51            client,
52            timeout: self.timeout,
53            retry_count: self.retry_count,
54            retry_delay: self.retry_delay,
55            log_level: self.log_level,
56        })
57    }
58}
59use crate::result::SmsIrResult;
60use crate::error::SmsIrError;
61use tracing::{info, error};
62use reqwest::{Client, Method};
63use serde_json::Value;
64
65
66#[derive(Clone)]
67pub struct SmsIrService {
68    api_key: String,
69    base_uri: String,
70    client: Client,
71    timeout: std::time::Duration,
72    retry_count: usize,
73    retry_delay: std::time::Duration,
74    log_level: LogLevel,
75}
76
77#[derive(Clone, Copy, Debug)]
78pub enum LogLevel {
79    Error,
80    Info,
81    Debug,
82    Off,
83}
84
85#[async_trait::async_trait]
86pub trait SmsService {
87    async fn get_credit(&self) -> Result<SmsIrResult, SmsIrError>;
88    async fn get_lines(&self) -> Result<SmsIrResult, SmsIrError>;
89    async fn get_latest_receives(&self, count: u32) -> Result<SmsIrResult, SmsIrError>;
90    async fn get_live_receives(&self, page_number: u32, page_size: u32, sort_by_newest: bool) -> Result<SmsIrResult, SmsIrError>;
91    async fn get_archived_receives(&self, page_number: u32, page_size: u32, from_date: Option<u64>, to_date: Option<u64>) -> Result<SmsIrResult, SmsIrError>;
92    async fn bulk_send(&self, line_number: &str, message_text: &str, mobiles: &[String], send_date_time: Option<u64>) -> Result<SmsIrResult, SmsIrError>;
93    async fn like_to_like_send(&self, line_number: &str, message_texts: &[String], mobiles: &[String], send_date_time: Option<u64>) -> Result<SmsIrResult, SmsIrError>;
94    async fn verify_send(&self, mobile: &str, template_id: u32, parameters: &[serde_json::Value]) -> Result<SmsIrResult, SmsIrError>;
95    async fn remove_scheduled_messages(&self, pack_id: &str) -> Result<SmsIrResult, SmsIrError>;
96    async fn get_report_by_message_id(&self, message_id: &str) -> Result<SmsIrResult, SmsIrError>;
97    async fn get_report_by_pack_id(&self, pack_id: &str) -> Result<SmsIrResult, SmsIrError>;
98    async fn get_live_report(&self, page_number: u32, page_size: u32, sort_by_newest: bool) -> Result<SmsIrResult, SmsIrError>;
99    async fn get_archived_report(&self, page_number: u32, page_size: u32, from_date: Option<u64>, to_date: Option<u64>, sort_by_newest: bool) -> Result<SmsIrResult, SmsIrError>;
100    async fn get_send_packs(&self, page_number: u32, page_size: u32) -> Result<SmsIrResult, SmsIrError>;
101}
102
103impl SmsIrService {
104    /// Create a new SmsIrService with optional timeout, proxy, and custom headers.
105    ///
106    /// # Example (Recommended)
107    /// ```rust
108    /// use smsir_rust::service::SmsIrServiceBuilder;
109    /// let service = SmsIrServiceBuilder::new("API_KEY")
110    ///     .timeout(std::time::Duration::from_secs(10))
111    ///     .log_level(smsir_rust::service::LogLevel::Debug)
112    ///     .build()
113    ///     .unwrap();
114    /// ```
115    // Deprecated: Use SmsIrServiceBuilder instead
116    #[deprecated(note = "Use SmsIrServiceBuilder for easier configuration")]
117    pub fn new(
118        api_key: impl Into<String>,
119        base_uri: Option<&str>,
120        timeout: Option<std::time::Duration>,
121        proxy: Option<&str>,
122        retry_count: Option<usize>,
123        retry_delay: Option<std::time::Duration>,
124        log_level: Option<LogLevel>,
125    ) -> Result<Self, SmsIrError> {
126        let api_key_str = api_key.into();
127        if api_key_str.trim().is_empty() {
128            return Err(SmsIrError::InvalidApiKey("API key is empty".to_string()));
129        }
130        let mut client_builder = reqwest::Client::builder();
131        if let Some(proxy_url) = proxy {
132            client_builder = client_builder.proxy(reqwest::Proxy::all(proxy_url).expect("Invalid proxy"));
133        }
134        let client = client_builder.build().expect("Failed to build client");
135        Ok(Self {
136            api_key: api_key_str,
137            base_uri: base_uri.unwrap_or("https://api.sms.ir/v1/").to_string(),
138            client,
139            timeout: timeout.unwrap_or(std::time::Duration::from_secs(30)),
140            retry_count: retry_count.unwrap_or(3),
141            retry_delay: retry_delay.unwrap_or(std::time::Duration::from_millis(500)),
142            log_level: log_level.unwrap_or(LogLevel::Info),
143        })
144    }
145
146    pub fn with_client(
147        api_key: impl Into<String>,
148        base_uri: Option<&str>,
149        client: reqwest::Client,
150        timeout: std::time::Duration,
151        retry_count: Option<usize>,
152        retry_delay: Option<std::time::Duration>,
153        log_level: Option<LogLevel>,
154    ) -> Result<Self, SmsIrError> {
155        let api_key_str = api_key.into();
156        if api_key_str.trim().is_empty() {
157            return Err(SmsIrError::InvalidApiKey("API key is empty".to_string()));
158        }
159        Ok(Self {
160            api_key: api_key_str,
161            base_uri: base_uri.unwrap_or("https://api.sms.ir/v1/").to_string(),
162            client,
163            timeout,
164            retry_count: retry_count.unwrap_or(2),
165            retry_delay: retry_delay.unwrap_or(std::time::Duration::from_millis(500)),
166            log_level: log_level.unwrap_or(LogLevel::Info),
167        })
168    }
169
170    async fn send_request(&self, method: Method, uri: &str, query: Option<&[(&str, Value)]>, json: Option<&Value>) -> Result<SmsIrResult, SmsIrError> {
171        let url = format!("{}{}", self.base_uri, uri);
172        let mut last_err = None;
173        for attempt in 0..=self.retry_count {
174            self.log(format!("Sending request: {} {} (attempt {})", method, url, attempt), LogLevel::Info);
175            let mut req = self.client.request(method.clone(), &url)
176                .header("X-API-KEY", &self.api_key)
177                .header("Accept", "application/json");
178            if let Some(q) = query {
179                req = req.query(&q);
180            }
181            if let Some(j) = json {
182                req = req.json(j);
183            }
184            let resp = match tokio::time::timeout(self.timeout, req.send()).await {
185                Ok(Ok(r)) => r,
186                Ok(Err(e)) => {
187                    self.log(format!("HTTP error: {}", e), LogLevel::Error);
188                    last_err = Some(SmsIrError::Http(e.to_string()));
189                    continue;
190                },
191                Err(_) => {
192                    self.log("Request timed out".to_string(), LogLevel::Error);
193                    last_err = Some(SmsIrError::Timeout);
194                    continue;
195                },
196            };
197            let http_status = resp.status().as_u16();
198            let body: Value = match tokio::time::timeout(self.timeout, resp.json()).await {
199                Ok(Ok(j)) => j,
200                Ok(Err(e)) => {
201                    self.log(format!("JSON error: {}", e), LogLevel::Error);
202                    last_err = Some(SmsIrError::Json(e.to_string()));
203                    continue;
204                },
205                Err(_) => {
206                    self.log("Response JSON timed out".to_string(), LogLevel::Error);
207                    last_err = Some(SmsIrError::Timeout);
208                    continue;
209                },
210            };
211
212            let status_code = body.get("status")
213                .and_then(|v| if v.is_i64() { v.as_i64().map(|i| i as i32) } else { v.as_str().and_then(|s| s.parse::<i32>().ok()) })
214                .unwrap_or(-9999);
215            let status_enum = crate::result::SendStatus::from_i32(status_code);
216            let message = body.get("message").and_then(|v| v.as_str()).unwrap_or("").to_string();
217            let data = body.get("data").cloned();
218            match status_enum {
219                crate::result::SendStatus::Success => {
220                    return Ok(SmsIrResult::new(status_code, message, data));
221                },
222                // Retryable system errors
223                crate::result::SendStatus::SystemError | crate::result::SendStatus::TooManyRequests => {
224                    self.log(format!("API status error: {:?} - {} (HTTP {}) - retrying", status_enum, message, http_status), LogLevel::Error);
225                    last_err = Some(SmsIrError::StatusError(status_enum, message.clone()));
226                    tokio::time::sleep(self.retry_delay).await;
227                    continue;
228                },
229                // API key errors
230                crate::result::SendStatus::InvalidApiKey | crate::result::SendStatus::ApiKeyDisabled | crate::result::SendStatus::ApiKeyIpRestricted => {
231                    self.log(format!("API key error: {:?} - {} (HTTP {})", status_enum, message, http_status), LogLevel::Error);
232                    return Err(SmsIrError::StatusError(status_enum, message));
233                },
234                // User/account errors
235                crate::result::SendStatus::UserDisabled | crate::result::SendStatus::UserSuspended => {
236                    self.log(format!("User/account error: {:?} - {} (HTTP {})", status_enum, message, http_status), LogLevel::Error);
237                    return Err(SmsIrError::StatusError(status_enum, message));
238                },
239                // All other known errors
240                _ => {
241                    self.log(format!("SmsIr status error: {:?} - {} (HTTP {})", status_enum, message, http_status), LogLevel::Error);
242                    return Err(SmsIrError::StatusError(status_enum, message));
243                }
244            }
245        }
246        Err(last_err.unwrap_or(SmsIrError::Unexpected("Unknown error after retries".to_string(), 0)))
247    }
248
249    fn log(&self, msg: String, level: LogLevel) {
250        match self.log_level {
251            LogLevel::Off => {},
252            LogLevel::Error => if matches!(level, LogLevel::Error) { error!("{}", msg); },
253            LogLevel::Info => if matches!(level, LogLevel::Error | LogLevel::Info) { info!("{}", msg); },
254            LogLevel::Debug => info!("{}", msg),
255        }
256    }
257
258    pub async fn get_credit(&self) -> Result<SmsIrResult, SmsIrError> {
259        self.send_request(Method::GET, "credit", None, None).await
260    }
261
262    pub async fn get_lines(&self) -> Result<SmsIrResult, SmsIrError> {
263        self.send_request(Method::GET, "line", None, None).await
264    }
265
266    #[cfg(feature = "receive")]
267    pub async fn get_latest_receives(&self, count: u32) -> Result<SmsIrResult, SmsIrError> {
268        let query = vec![("count", Value::from(count))];
269        self.send_request(Method::GET, "receive/latest", Some(&query), None).await
270    }
271
272    #[cfg(feature = "receive")]
273    pub async fn get_live_receives(&self, page_number: u32, page_size: u32, sort_by_newest: bool) -> Result<SmsIrResult, SmsIrError> {
274        let query = vec![
275            ("pageNumber", Value::from(page_number)),
276            ("pageSize", Value::from(page_size)),
277            ("sortByNewest", Value::from(sort_by_newest)),
278        ];
279        self.send_request(Method::GET, "receive/live", Some(&query), None).await
280    }
281
282    #[cfg(feature = "receive")]
283    pub async fn get_archived_receives(&self, page_number: u32, page_size: u32, from_date: Option<u64>, to_date: Option<u64>) -> Result<SmsIrResult, SmsIrError> {
284        let mut query = vec![
285            ("pageNumber", Value::from(page_number)),
286            ("pageSize", Value::from(page_size)),
287        ];
288        if let Some(fd) = from_date {
289            query.push(("fromDate", Value::from(fd)));
290        }
291        if let Some(td) = to_date {
292            query.push(("toDate", Value::from(td)));
293        }
294        self.send_request(Method::GET, "receive/archive", Some(&query), None).await
295    }
296
297    #[cfg(feature = "bulk_send")]
298    pub async fn bulk_send(&self, line_number: &str, message_text: &str, mobiles: &[String], send_date_time: Option<u64>) -> Result<SmsIrResult, SmsIrError> {
299        let mut payload = serde_json::json!({
300            "lineNumber": line_number,
301            "messageText": message_text,
302            "mobiles": mobiles,
303        });
304        if let Some(dt) = send_date_time {
305            payload["sendDateTime"] = Value::from(dt);
306        }
307        self.send_request(Method::POST, "send/bulk", None, Some(&payload)).await
308    }
309
310    #[cfg(feature = "bulk_send")]
311    pub async fn like_to_like_send(&self, line_number: &str, message_texts: &[String], mobiles: &[String], send_date_time: Option<u64>) -> Result<SmsIrResult, SmsIrError> {
312        let mut payload = serde_json::json!({
313            "lineNumber": line_number,
314            "messageTexts": message_texts,
315            "mobiles": mobiles,
316        });
317        if let Some(dt) = send_date_time {
318            payload["sendDateTime"] = Value::from(dt);
319        }
320        self.send_request(Method::POST, "send/likeTolike", None, Some(&payload)).await
321    }
322
323    #[cfg(feature = "verify_send")]
324    pub async fn verify_send(&self, mobile: &str, template_id: u32, parameters: &[serde_json::Value]) -> Result<SmsIrResult, SmsIrError> {
325        let mut params = Vec::new();
326        for param in parameters {
327            if let Some(obj) = param.as_object() {
328                for (k, v) in obj {
329                    params.push(serde_json::json!({
330                        "name": k,
331                        "value": v,
332                    }));
333                }
334            }
335        }
336        let payload = serde_json::json!({
337            "mobile": mobile,
338            "templateId": template_id,
339            "parameters": params,
340        });
341        self.send_request(Method::POST, "send/verify", None, Some(&payload)).await
342    }
343
344    #[cfg(feature = "bulk_send")]
345    pub async fn remove_scheduled_messages(&self, pack_id: &str) -> Result<SmsIrResult, SmsIrError> {
346        let uri = format!("send/scheduled/{}", pack_id);
347        self.send_request(Method::DELETE, &uri, None, None).await
348    }
349
350    #[cfg(feature = "report")]
351    pub async fn get_report_by_message_id(&self, message_id: &str) -> Result<SmsIrResult, SmsIrError> {
352        let uri = format!("send/{}", message_id);
353        self.send_request(Method::GET, &uri, None, None).await
354    }
355
356    #[cfg(feature = "report")]
357    pub async fn get_report_by_pack_id(&self, pack_id: &str) -> Result<SmsIrResult, SmsIrError> {
358        let uri = format!("send/pack/{}", pack_id);
359        self.send_request(Method::GET, &uri, None, None).await
360    }
361
362    #[cfg(feature = "report")]
363    pub async fn get_live_report(&self, page_number: u32, page_size: u32, sort_by_newest: bool) -> Result<SmsIrResult, SmsIrError> {
364        let query = vec![
365            ("pageNumber", Value::from(page_number)),
366            ("pageSize", Value::from(page_size)),
367            ("sortByNewest", Value::from(sort_by_newest)),
368        ];
369        self.send_request(Method::GET, "send/live", Some(&query), None).await
370    }
371
372    #[cfg(feature = "report")]
373    pub async fn get_archived_report(&self, page_number: u32, page_size: u32, from_date: Option<u64>, to_date: Option<u64>, sort_by_newest: bool) -> Result<SmsIrResult, SmsIrError> {
374        let mut query = vec![
375            ("pageNumber", Value::from(page_number)),
376            ("pageSize", Value::from(page_size)),
377            ("sortByNewest", Value::from(sort_by_newest)),
378        ];
379        if let Some(fd) = from_date {
380            query.push(("fromDate", Value::from(fd)));
381        }
382        if let Some(td) = to_date {
383            query.push(("toDate", Value::from(td)));
384        }
385        self.send_request(Method::GET, "send/archive", Some(&query), None).await
386    }
387
388    #[cfg(feature = "report")]
389    pub async fn get_send_packs(&self, page_number: u32, page_size: u32) -> Result<SmsIrResult, SmsIrError> {
390        let query = vec![
391            ("pageNumber", Value::from(page_number)),
392            ("pageSize", Value::from(page_size)),
393        ];
394        self.send_request(Method::GET, "send/pack", Some(&query), None).await
395    }
396}