niazpardaz-sms 1.0.3

Official Rust SDK for Niazpardaz SMS API | کتابخانه رسمی Rust برای API پیامکی نیازپرداز
Documentation
use crate::errors::Result;
use crate::models::*;
use serde::de::DeserializeOwned;
use serde_json::json;
use std::time::Duration;

const DEFAULT_BASE_URL: &str = "https://login.niazpardaz.ir/api/v2/RestWebApi";
const DEFAULT_TIMEOUT_SECS: u64 = 30;

/// کلاینت blocking (سنکرون) پیامکی نیازپرداز
///
/// مناسب برای برنامه‌هایی که از async runtime استفاده نمی‌کنند.
///
/// ```rust,no_run
/// use niazpardaz_sms::BlockingClient;
///
/// let client = BlockingClient::new("YOUR_API_KEY");
/// let result = client.send("10001234", "09123456789", "سلام!").unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct BlockingClient {
    api_key: String,
    base_url: String,
    http_client: reqwest::blocking::Client,
}

impl BlockingClient {
    /// ساخت کلاینت جدید
    pub fn new(api_key: impl Into<String>) -> Self {
        let http_client = reqwest::blocking::Client::builder()
            .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
            .build()
            .expect("Failed to create HTTP client");

        Self {
            api_key: api_key.into().trim().to_string(),
            base_url: DEFAULT_BASE_URL.to_string(),
            http_client,
        }
    }

    /// تنظیم آدرس پایه
    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = url.into();
        self
    }

    /// تنظیم HTTP Client سفارشی
    pub fn with_http_client(mut self, client: reqwest::blocking::Client) -> Self {
        self.http_client = client;
        self
    }

    // ─── Send Methods ────────────────────────────────

    pub fn send(&self, from: &str, to: &str, message: &str) -> Result<SendBatchSmsResult> {
        self.send_bulk(from, &[to], message)
    }

    pub fn send_bulk(
        &self,
        from: &str,
        to_numbers: &[&str],
        message: &str,
    ) -> Result<SendBatchSmsResult> {
        self.send_bulk_full(from, to_numbers, message, false, None)
    }

    pub fn send_bulk_full(
        &self,
        from: &str,
        to_numbers: &[&str],
        message: &str,
        is_flash: bool,
        send_delay: Option<i32>,
    ) -> Result<SendBatchSmsResult> {
        let mut payload = json!({
            "fromNumber": from,
            "messageContent": message,
            "toNumbers": to_numbers.join(","),
            "isFlash": is_flash,
        });
        if let Some(delay) = send_delay {
            payload["sendDelay"] = json!(delay);
        }
        self.post("/SendBatchSms", &payload)
    }

    pub fn send_sms_like_to_like(
        &self,
        from: &str,
        to_numbers: &[&str],
        messages: &[&str],
    ) -> Result<SendLikeToLikeResult> {
        let payload = json!({
            "fromNumber": from,
            "messageContents": messages.join(","),
            "toNumbers": to_numbers.join(","),
            "isFlash": false,
        });
        self.post("/SendSmsLikeToLike", &payload)
    }

    pub fn send_voice_otp(&self, from: &str, to: &str, otp: &str) -> Result<SendBatchSmsResult> {
        let payload = json!({
            "fromNumber": from, "messageContent": otp,
            "toNumbers": to, "isFlash": false,
        });
        self.post("/SendVoiceOtp", &payload)
    }

    // ─── Delivery Reports ────────────────────────────

    pub fn get_batch_delivery(
        &self,
        batch_sms_id: i64,
        page_index: i32,
        page_size: i32,
    ) -> Result<BatchDeliveryResult> {
        let payload =
            json!({ "batchSmsId": batch_sms_id, "index": page_index, "count": page_size });
        self.post("/GetBatchDelivery", &payload)
    }

    pub fn get_delivery_like_to_like(&self, sms_id: i64) -> Result<BatchDeliveryResult> {
        self.post("/GetDeliveryLikeToLike", &json!({ "smsId": sms_id }))
    }

    // ─── Account Info ────────────────────────────────

    pub fn get_credit(&self) -> Result<CreditResult> {
        self.post("/GetCredit", &json!({}))
    }

    pub fn get_sender_numbers(&self) -> Result<SenderNumbersResult> {
        self.post("/GetSenderNumbers", &json!({}))
    }

    pub fn get_inbox_count(&self, is_read: bool) -> Result<InboxCountResult> {
        self.post("/GetInboxCount", &json!({ "isRead": is_read }))
    }

    // ─── Messages ────────────────────────────────────

    pub fn get_messages(
        &self,
        message_type: i32,
        from_numbers: &str,
        page_index: i32,
        page_size: i32,
    ) -> Result<MessagesResult> {
        let payload = json!({ "messageType": message_type, "fromNumbers": from_numbers, "index": page_index, "count": page_size });
        self.post("/GetMessages", &payload)
    }

    pub fn get_messages_by_date_range(
        &self,
        message_type: i32,
        from_numbers: &str,
        from_date: &str,
        to_date: &str,
    ) -> Result<MessagesResult> {
        let payload = json!({ "messageType": message_type, "fromNumbers": from_numbers, "fromDate": from_date, "toDate": to_date });
        self.post("/GetMessagesByDateRange", &payload)
    }

    // ─── Blacklist & Content ─────────────────────────

    pub fn extract_telecom_blacklist_numbers(
        &self,
        numbers: &[&str],
    ) -> Result<BlacklistNumbersResult> {
        self.post(
            "/ExtractTelecomBlacklistNumbers",
            &json!({ "numbers": numbers.join(",") }),
        )
    }

    pub fn number_is_in_telecom_blacklist(&self, number: &str) -> Result<IsBlacklistResult> {
        self.post("/NumberIsInTelecomBlacklist", &json!({ "number": number }))
    }

    pub fn check_sms_content(&self, message: &str) -> Result<CheckContentResult> {
        self.post("/CheckSmsContent", &json!({ "message": message }))
    }

    // ─── HTTP Layer ──────────────────────────────────

    fn post<T: DeserializeOwned>(&self, endpoint: &str, payload: &serde_json::Value) -> Result<T> {
        let url = format!("{}{}", self.base_url, endpoint);

        let response = self
            .http_client
            .post(&url)
            .header("X-API-Key", &self.api_key)
            .header("Content-Type", "application/json; charset=UTF-8")
            .header("Accept", "application/json")
            .json(payload)
            .send()?;

        let status = response.status();
        let body = response.text()?;

        if !status.is_success() {
            return Err(crate::NiazpardazError::General(format!(
                "HTTP {}",
                status.as_u16()
            )));
        }

        let api_response: ApiResponse = serde_json::from_str(&body)?;

        if !api_response.success {
            let code = api_response
                .result
                .get("resultCode")
                .and_then(|v| v.as_i64())
                .unwrap_or(0) as i32;
            let msg = api_response
                .error_message
                .unwrap_or_else(|| "خطای نامشخص".to_string());
            return Err(crate::NiazpardazError::Api { code, message: msg });
        }

        Ok(serde_json::from_value(api_response.result)?)
    }
}