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 }
}
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
}
}