spring-sms 0.2.0

基于spring-rs版,简单实现了短信发送功能,未来将完善,目前只支持阿里云短信发送
Documentation
use crate::config::AliyunSmsConfig;
use base64::{engine::general_purpose, Engine as _};
use chrono::Local;
use hmac::{Hmac, Mac};
use reqwest::Client;
use sha1::Sha1;
use std::collections::HashMap;
use std::error::Error;
use std::ops::{Add, Deref};
use std::time::{SystemTime, UNIX_EPOCH};

type HmacSha1 = Hmac<Sha1>;

#[derive(Debug, Clone)]
pub struct Aliyun<'a> {
    config: &'a AliyunSmsConfig,
}

impl<'a> Aliyun<'a> {
    pub fn new(config: &'a AliyunSmsConfig) -> Self {
        Self { config }
    }

    /// Send Aliyun Sms
    ///
    /// `PhoneNumbers` Supports both individual and batch phone numbers,
    ///  with batch phone numbers being split and used separately
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```
    /// use std::collections::HashMap;
    /// use spring_sms::aliyun::Aliyun;
    /// use spring_sms::config::AliyunSmsConfig;
    /// use std::error::Error;
    ///
    /// async fn send() -> Result<(), Box<dyn Error>> {
    ///     let config = &AliyunSmsConfig {
    ///         access_key_id: "".to_string(),
    ///         access_key_secret: "".to_string(),
    ///         sign_name: "".to_string(),
    ///         domain: None,
    ///         region_id: None,
    ///         version: None,
    ///     };
    ///     let sms_client = Aliyun::new(config);
    ///
    ///     let mut template_params = HashMap::new();
    ///     template_params.insert("code", "123456");
    ///
    ///     let _ = sms_client.send_sms("130****8888,139****8888", "SMS_123456789", Some(template_params)).await;
    ///
    ///     Ok(())
    /// }
    ///
    /// ```
    pub async fn send_sms(
        &self,
        phone_numbers: &str,
        template_code: &str,
        template_param: Option<HashMap<&str, &str>>,
    ) -> Result<(), Box<dyn Error>> {
        let template_param = template_param.map(|p| serde_json::to_string(&p).unwrap());
        let mut data = HashMap::new();
        data.insert("PhoneNumbers", phone_numbers);
        data.insert("TemplateCode", template_code);
        data.insert("SignName", &self.config.sign_name);
        if let Some(param) = &template_param {
            data.insert("TemplateParam", param.deref());
        }
        let response = self.request("SendSms", &data).await?;
        if response.get("Code") != Some(&"OK".to_string()) {
            return Err(response.get("Message").unwrap_or(&"failed to send sms".to_string()).clone().into());
        }

        Ok(())
    }


    async fn request(&self, action: &str, data: &HashMap<&str, &str>) -> Result<HashMap<String, String>, Box<dyn Error>> {
        let domain = &self.domain();
        let version = &self.version();
        let access_key_id = &self.access_key_id();
        let mut api_params = HashMap::new();
        let signature_nonce = self.generate_nonce();
        let timestamp = self.generate_timestamp();
        api_params.insert("SignatureMethod", "HMAC-SHA1");
        api_params.insert("SignatureNonce", &*signature_nonce);
        api_params.insert("SignatureVersion", "1.0");
        api_params.insert("Timestamp", &*timestamp);
        api_params.insert("Format", "JSON");
        api_params.insert("Version", version);
        api_params.insert("AccessKeyId", access_key_id);
        api_params.insert("Action", action);
        for (key, value) in data {
            api_params.insert(key, value);
        }
        let query_string = self.encode_params(&api_params);
        let signature = self.generate_signature(&query_string);
        let url = format!("{}://{}/?Signature={}&{}", "https", domain, self.url_encode(&signature), query_string);
        let client = Client::new();
        let response = client.get(&url).send().await?.json::<HashMap<String, String>>().await?;
        Ok(response)
    }

    fn url_encode(&self, input: &str) -> String {
        urlencoding::encode(input).replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
    }
    fn generate_signature(&self, query_string: &str) -> String {
        let string_to_sign = format!("GET&%2F&{}", self.url_encode(query_string));
        let mut mac = HmacSha1::new_from_slice(&self.config.access_key_secret.clone().add("&").as_bytes()).expect("HMAC can take key of any size");
        mac.update(string_to_sign.as_bytes());
        let result = mac.finalize();
        let code_bytes = result.into_bytes();
        general_purpose::STANDARD.encode(&code_bytes)
    }
    fn encode_params(&self, params: &HashMap<&str, &str>) -> String {
        let mut sorted_params: Vec<(&&str, &&str)> = params.iter().collect();
        sorted_params.sort_by(|a, b| a.0.cmp(b.0));
        let query_string: String = sorted_params.iter()
            .map(|(k, v)| format!("{}={}", self.url_encode(k), self.url_encode(v)))
            .collect::<Vec<String>>()
            .join("&");
        query_string
    }
    fn generate_timestamp(&self) -> String {
        let now = SystemTime::now();
        let since_epoch = now.duration_since(UNIX_EPOCH).unwrap();
        let datetime = chrono::DateTime::<chrono::Utc>::from(SystemTime::UNIX_EPOCH + since_epoch);
        datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string()
    }
    fn generate_nonce(&self) -> String {
        format!("{}{}", Local::now().timestamp() as u64, rand::random::<u8>())
    }
    fn domain(&self) -> String {
        self.config.clone().domain.unwrap_or("dysmsapi.aliyuncs.com".to_string())
    }
    fn version(&self) -> String {
        self.config.clone().version.unwrap_or("2017-05-25".to_string())
    }
    fn access_key_id(&self) -> String {
        self.config.clone().access_key_id
    }
}