ippanel-sms 0.1.0

A professional, asynchronous Rust client library for the IPPANEL SMS web service.
Documentation
use reqwest::Client;

/// IPPanel SMS API client.
///
/// Provides methods to interact with the IPPanel SMS web service, including sending messages, checking credit, managing patterns, and more.
pub struct IppanelClient {
    pub api_key: String,
    pub base_url: String,
    client: Client,
}

impl IppanelClient {
    /// Create a new `IppanelClient` instance.
    ///
    /// # Arguments
    /// * `api_key` - The API key for authentication (optional).
    /// * `base_url` - The base URL for the API (optional, defaults to official endpoint).
    pub fn new(api_key: Option<String>, base_url: Option<String>) -> Self {
        Self {
            api_key: api_key.unwrap_or_else(|| "".to_string()),
            base_url: base_url.unwrap_or_else(|| "https://api2.ippanel.com/api/v1".to_string()),
            client: Client::new(),
        }
    }

    /// Set the base URL for the API.
    pub fn set_base_url(&mut self, base_url: String) {
        self.base_url = base_url;
    }

    /// Set the API key for authentication.
    pub fn set_api_key(&mut self, api_key: String) {
        self.api_key = api_key;
    }

    /// Get the current account credit.
    ///
    /// Returns the available credit as a floating-point number.
    pub async fn get_credit(&self) -> Result<f64, crate::error::IppanelError> {
        let url = format!("{}/sms/accounting/credit/show", self.base_url);
        let res = self
            .client
            .get(&url)
            .header("apikey", &self.api_key)
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;

        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;

        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }

        if let Some(credit) = json.get("data").and_then(|d| d.get("credit")).and_then(|c| c.as_f64()) {
            Ok(credit)
        } else {
            Err(crate::error::IppanelError::new("Invalid response: missing credit field", crate::error::ErrorCode::Unknown.code()))
        }
    }


    /// Send a message from the originator to multiple recipients.
    ///
    /// # Arguments
    /// * `originator` - The sender number.
    /// * `recipients` - List of recipient numbers.
    /// * `message` - The message body.
    /// * `summary` - Description or summary for the message.
    ///
    /// Returns the message tracking code.
    pub async fn send(&self, originator: &str, recipients: &[&str], message: &str, summary: &str) -> Result<i64, crate::error::IppanelError> {
        let url = format!("{}/sms/send/webservice/single", self.base_url);
        let recipients_vec: Vec<String> = recipients.iter().map(|s| s.to_string()).collect();
        let body = serde_json::json!({
            "sender": originator,
            "recipient": recipients_vec,
            "message": message,
            "description": {
                "summary": summary,
                "count_recipient": recipients.len().to_string()
            }
        });
        let res = self
            .client
            .post(&url)
            .header("apikey", &self.api_key)
            .json(&body)
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }
        if let Some(message_id) = json.get("data").and_then(|d| d.get("message_id")).and_then(|id| id.as_i64()) {
            Ok(message_id)
        } else {
            Err(crate::error::IppanelError::new("Invalid response: missing message_id field", crate::error::ErrorCode::Unknown.code()))
        }
    }


    /// Get brief information about a sent message.
    ///
    /// # Arguments
    /// * `message_id` - The message tracking code.
    ///
    /// Returns a `Message` struct with message details.
    pub async fn get_message(&self, message_id: i64) -> Result<crate::models::message::Message, crate::error::IppanelError> {
        let url = format!("{}/sms/message/all", self.base_url);
        let res = self
            .client
            .get(&url)
            .header("apikey", &self.api_key)
            .query(&[("message_id", message_id)])
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }
        if let Some(data) = json.get("data").and_then(|d| d.as_array()).and_then(|arr| arr.get(0)) {
            let msg: crate::models::message::Message = serde_json::from_value(data.clone())
                .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
            Ok(msg)
        } else {
            Err(crate::error::IppanelError::new("Invalid response: missing message data", crate::error::ErrorCode::Unknown.code()))
        }
    }


    /// Fetch delivery statuses for message recipients.
    ///
    /// # Arguments
    /// * `message_id` - The message tracking code.
    /// * `page` - Page number (starting from 0).
    /// * `limit` - Number of results per page.
    ///
    /// Returns a tuple of recipient statuses and pagination info.
    pub async fn fetch_statuses(&self, message_id: i64, page: u32, limit: u32) -> Result<(Vec<crate::models::recipient::Recipient>, crate::models::pagination_info::PaginationInfo), crate::error::IppanelError> {
        let url = format!("{}/sms/message/show-recipient/message-id/{}", self.base_url, message_id);
        let res = self
            .client
            .get(&url)
            .header("apikey", &self.api_key)
            .query(&[("page", page), ("per_page", limit)])
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }
        let mut recipients = Vec::new();
        if let Some(deliveries) = json.get("data").and_then(|d| d.get("deliveries")).and_then(|d| d.as_array()) {
            for r in deliveries {
                let rec: crate::models::recipient::Recipient = serde_json::from_value(r.clone())
                    .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
                recipients.push(rec);
            }
        }
        let mut pagination = crate::models::pagination_info::PaginationInfo {
            total: 0,
            limit: 0,
            page: 0,
            pages: 0,
            prev: None,
            next: None,
        };
        if let Some(meta) = json.get("meta") {
            if let Ok(p) = serde_json::from_value(meta.clone()) {
                pagination = p;
            }
        }
        Ok((recipients, pagination))
    }


    /// Fetch received inbox messages.
    ///
    /// # Arguments
    /// * `page` - Page number (starting from 1).
    /// * `limit` - Number of results per page.
    ///
    /// Returns a tuple of inbox messages and pagination info.
    pub async fn fetch_inbox(&self, page: u32, limit: u32) -> Result<(Vec<crate::models::inbox_message::InboxMessage>, crate::models::pagination_info::PaginationInfo), crate::error::IppanelError> {
        let url = format!("{}/inbox", self.base_url);
        let res = self
            .client
            .get(&url)
            .header("apikey", &self.api_key)
            .query(&[("page", page), ("per_page", limit)])
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }
        let mut messages = Vec::new();
        if let Some(data) = json.get("data").and_then(|d| d.as_array()) {
            for r in data {
                let msg: crate::models::inbox_message::InboxMessage = serde_json::from_value(r.clone())
                    .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
                messages.push(msg);
            }
        }
        let mut pagination = crate::models::pagination_info::PaginationInfo {
            total: 0,
            limit: 0,
            page: 0,
            pages: 0,
            prev: None,
            next: None,
        };
        if let Some(meta) = json.get("meta") {
            if let Ok(p) = serde_json::from_value(meta.clone()) {
                pagination = p;
            }
        }
        Ok((messages, pagination))
    }


    /// Create a new message pattern (template).
    ///
    /// # Arguments
    /// * `pattern` - The pattern schema.
    /// * `description` - Pattern description.
    /// * `variables` - List of variable names and types.
    /// * `delimiter` - Variable delimiter (default: "%").
    /// * `is_shared` - Whether the pattern is shared.
    ///
    /// Returns the pattern code as a string.
    pub async fn create_pattern(&self, pattern: &str, description: &str, variables: &[(String, String)], delimiter: &str, is_shared: bool) -> Result<String, crate::error::IppanelError> {
        let url = format!("{}/sms/pattern/normal/store", self.base_url);
        let vars: Vec<serde_json::Value> = variables.iter().map(|(name, typ)| serde_json::json!({"name": name, "type": typ})).collect();
        let body = serde_json::json!({
            "message": pattern,
            "delimiter": delimiter,
            "description": description,
            "variable": vars,
            "is_share": is_shared,
        });
        let res = self
            .client
            .post(&url)
            .header("apikey", &self.api_key)
            .json(&body)
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }
        if let Some(code) = json.get("data").and_then(|d| d.as_array()).and_then(|arr| arr.get(0)).and_then(|v| v.get("code")).and_then(|c| c.as_str()) {
            Ok(code.to_string())
        } else {
            Err(crate::error::IppanelError::new("Invalid response: missing pattern code", crate::error::ErrorCode::Unknown.code()))
        }
    }


    /// Send a message using a pattern (template).
    ///
    /// # Arguments
    /// * `pattern_code` - The pattern code.
    /// * `originator` - The sender number.
    /// * `recipient` - The recipient number.
    /// * `variables` - Pattern variable values.
    ///
    /// Returns the message tracking code.
    pub async fn send_pattern(&self, pattern_code: &str, originator: &str, recipient: &str, variables: &[(String, String)]) -> Result<i64, crate::error::IppanelError> {
        let url = format!("{}/sms/pattern/normal/send", self.base_url);
        let vars: std::collections::HashMap<String, String> = variables.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
        let body = serde_json::json!({
            "code": pattern_code,
            "sender": originator,
            "recipient": recipient,
            "variable": vars,
        });
        let res = self
            .client
            .post(&url)
            .header("apikey", &self.api_key)
            .json(&body)
            .send()
            .await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        let status = res.status();
        let json: serde_json::Value = res.json().await
            .map_err(|e| crate::error::IppanelError::new(e.to_string(), crate::error::ErrorCode::Unknown.code()))?;
        if !status.is_success() {
            let code = json.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let msg = json.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("API error");
            return Err(crate::error::IppanelError::new(msg, code));
        }
        if let Some(message_id) = json.get("data").and_then(|d| d.get("message_id")).and_then(|id| id.as_i64()) {
            Ok(message_id)
        } else {
            Err(crate::error::IppanelError::new("Invalid response: missing message_id field", crate::error::ErrorCode::Unknown.code()))
        }
    }
}