niazpardaz-sms 1.0.3

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

use crate::errors::{NiazpardazError, Result};
use crate::models::*;

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

/// کلاینت async پیامکی نیازپرداز
///
/// # نمونه استفاده
///
/// ```rust,no_run
/// use niazpardaz_sms::NiazpardazClient;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = NiazpardazClient::new("YOUR_API_KEY");
/// let result = client.send("10001234", "09123456789", "سلام!").await?;
/// println!("Success: {}", result.is_successful());
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct NiazpardazClient {
    api_key: String,
    base_url: String,
    http_client: Client,
}

/// تنظیمات ساخت کلاینت
pub struct ClientBuilder {
    api_key: String,
    base_url: String,
    timeout: Duration,
    http_client: Option<Client>,
}

impl ClientBuilder {
    /// شروع ساخت کلاینت
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into().trim().to_string(),
            base_url: DEFAULT_BASE_URL.to_string(),
            timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
            http_client: None,
        }
    }

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

    /// تنظیم تایم‌اوت
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    /// استفاده از HTTP Client سفارشی
    pub fn http_client(mut self, client: Client) -> Self {
        self.http_client = Some(client);
        self
    }

    /// ساخت کلاینت
    pub fn build(self) -> NiazpardazClient {
        let http_client = self.http_client.unwrap_or_else(|| {
            Client::builder()
                .timeout(self.timeout)
                .build()
                .expect("Failed to create HTTP client")
        });

        NiazpardazClient {
            api_key: self.api_key,
            base_url: self.base_url,
            http_client,
        }
    }
}

impl NiazpardazClient {
    /// ساخت کلاینت جدید با API Key
    pub fn new(api_key: impl Into<String>) -> Self {
        ClientBuilder::new(api_key).build()
    }

    /// ساخت کلاینت با Builder Pattern
    pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
        ClientBuilder::new(api_key)
    }

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

    /// ارسال پیامک تکی
    pub async fn send(&self, from: &str, to: &str, message: &str) -> Result<SendBatchSmsResult> {
        self.send_bulk(from, &[to], message).await
    }

    /// ارسال پیامک گروهی
    pub async fn send_bulk(
        &self,
        from: &str,
        to_numbers: &[&str],
        message: &str,
    ) -> Result<SendBatchSmsResult> {
        self.send_bulk_full(from, to_numbers, message, false, None)
            .await
    }

    /// ارسال پیامک گروهی با تنظیمات کامل
    pub async 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).await
    }

    /// ارسال پیامک نظیر به نظیر
    pub async fn send_sms_like_to_like(
        &self,
        from: &str,
        to_numbers: &[&str],
        messages: &[&str],
    ) -> Result<SendLikeToLikeResult> {
        self.send_sms_like_to_like_full(from, to_numbers, messages, false)
            .await
    }

    /// ارسال نظیر به نظیر با تنظیمات کامل
    pub async fn send_sms_like_to_like_full(
        &self,
        from: &str,
        to_numbers: &[&str],
        messages: &[&str],
        is_flash: bool,
    ) -> Result<SendLikeToLikeResult> {
        let payload = json!({
            "fromNumber": from,
            "messageContents": messages.join(","),
            "toNumbers": to_numbers.join(","),
            "isFlash": is_flash,
        });

        self.post("/SendSmsLikeToLike", &payload).await
    }

    /// ارسال OTP صوتی
    pub async 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).await
    }

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

    /// گزارش تحویل گروهی
    pub async 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).await
    }

    /// گزارش تحویل نظیر به نظیر
    pub async fn get_delivery_like_to_like(&self, sms_id: i64) -> Result<BatchDeliveryResult> {
        let payload = json!({ "smsId": sms_id });
        self.post("/GetDeliveryLikeToLike", &payload).await
    }

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

    /// دریافت اعتبار باقیمانده
    pub async fn get_credit(&self) -> Result<CreditResult> {
        self.post("/GetCredit", &json!({})).await
    }

    /// دریافت شماره‌های فرستنده
    pub async fn get_sender_numbers(&self) -> Result<SenderNumbersResult> {
        self.post("/GetSenderNumbers", &json!({})).await
    }

    /// تعداد پیام‌های دریافتی
    pub async fn get_inbox_count(&self, is_read: bool) -> Result<InboxCountResult> {
        let payload = json!({ "isRead": is_read });
        self.post("/GetInboxCount", &payload).await
    }

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

    /// دریافت لیست پیامک‌ها
    pub async 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).await
    }

    /// پیامک‌ها بر اساس بازه زمانی
    pub async 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).await
    }

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

    /// استخراج شماره‌های لیست سیاه مخابرات
    pub async fn extract_telecom_blacklist_numbers(
        &self,
        numbers: &[&str],
    ) -> Result<BlacklistNumbersResult> {
        let payload = json!({ "numbers": numbers.join(",") });
        self.post("/ExtractTelecomBlacklistNumbers", &payload).await
    }

    /// بررسی شماره در لیست سیاه
    pub async fn number_is_in_telecom_blacklist(&self, number: &str) -> Result<IsBlacklistResult> {
        let payload = json!({ "number": number });
        self.post("/NumberIsInTelecomBlacklist", &payload).await
    }

    /// بررسی محتوای پیامک
    pub async fn check_sms_content(&self, message: &str) -> Result<CheckContentResult> {
        let payload = json!({ "message": message });
        self.post("/CheckSmsContent", &payload).await
    }

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

    async 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()
            .await?;

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

        if !status.is_success() {
            return Err(NiazpardazError::General(format!(
                "خطا در ارتباط با سرور: HTTP {}",
                status.as_u16()
            )));
        }

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

        if !api_response.success {
            let error_code = api_response
                .result
                .get("resultCode")
                .and_then(|v| v.as_i64())
                .unwrap_or(0) as i32;

            let message = api_response
                .error_message
                .unwrap_or_else(|| "خطای نامشخص".to_string());

            return Err(NiazpardazError::Api {
                code: error_code,
                message,
            });
        }

        let result: T = serde_json::from_value(api_response.result)?;
        Ok(result)
    }
}