Skip to main content

lsys_lib_sms/sms_lib/
sender_aliyun.rs

1//! # aliyun SMS
2//!
3//! **阿里云短信sdk**
4//!
5//! 目前实现了发送短信功能
6//!
7
8use crate::{
9    now_time, rand_str,
10    sms_lib::{phone_numbers_check, response_check, SendStatus},
11    BranchSendNotifyResult, SendNotifyItem, SendNotifyStatus,
12};
13use hmac::{Hmac, Mac};
14
15use chrono::{DateTime, NaiveDateTime, Utc};
16use reqwest::{
17    header::{HeaderMap, HeaderValue},
18    Client, Method,
19};
20use serde_json::json;
21use sha2::{Digest, Sha256};
22use std::collections::HashMap;
23use tracing::debug;
24
25use super::{
26    response_msg, BranchSendDetailResult, BranchSendResult, SendDetailItem, SendError,
27    SendResultItem,
28};
29/// aliyun sms
30pub struct AliSms {}
31
32impl AliSms {
33    pub fn send_notify_output(res: &Result<(), String>) -> String {
34        match res {
35            Ok(_) => {
36                json!({
37                  "code" : 0,
38                  "msg" : "接收成功"
39                })
40            }
41            Err(err) => {
42                json!({
43                  "code" : 400,
44                  "msg" : err
45                })
46            }
47        }
48        .to_string()
49    }
50    //post json
51    pub fn send_notify_parse(notify_data: &str) -> BranchSendNotifyResult {
52        //         [
53        //   {
54        //     "phone_number" : "1381111****",
55        //     "send_time" : "2017-01-01 00:00:00",
56        //     "report_time" : "2017-01-01 00:00:00",
57        //     "success" : true,
58        //     "err_code" : "DELIVERED",
59        //     "err_msg" : "用户接收成功",
60        //     "sms_size" : "1",
61        //     "biz_id" : "12345",
62        //     "out_id" : "67890"
63        //   }
64        // ]
65        // println!("{}", notify_data);
66        let items = gjson::parse(notify_data);
67        let mut out = Vec::with_capacity(items.array().len());
68        for tmp in items.array() {
69            let send_time = chrono::NaiveDateTime::parse_from_str(
70                &tmp.get("send_time").to_string(),
71                "%Y-%m-%d %H:%M:%S",
72            )
73            .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
74            .ok();
75            let receive_time = chrono::NaiveDateTime::parse_from_str(
76                &tmp.get("report_time").to_string(),
77                "%Y-%m-%d %H:%M:%S",
78            )
79            .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
80            .ok();
81
82            out.push(SendNotifyItem {
83                status: if tmp.get("success").bool() {
84                    SendNotifyStatus::Completed
85                } else {
86                    SendNotifyStatus::Failed
87                },
88                message: tmp.get("err_msg").to_string(),
89                send_time,
90                receive_time,
91                code: tmp.get("err_code").to_string(),
92                send_id: tmp.get("biz_id").to_string(),
93                mobile: Some(tmp.get("phone_number").to_string()),
94            });
95        }
96        Ok(out)
97    }
98    pub async fn send_detail(
99        client: Client,
100        access_key_id: &str,
101        access_secret: &str,
102        send_id: &str,
103        mobile: &str,
104        send_date: &str,
105    ) -> BranchSendDetailResult {
106        let mut params = HashMap::new();
107        params.insert("PhoneNumber", mobile);
108        params.insert("BizId", send_id);
109        params.insert("SendDate", send_date);
110        params.insert("PageSize", "1");
111        params.insert("CurrentPage", "1");
112        params.insert("Version", "2017-05-25");
113        params.insert("Action", "QuerySendDetails");
114
115        let res = Self::build_request(
116            client,
117            Method::POST,
118            "dysmsapi.aliyuncs.com",
119            "/",
120            "",
121            "QuerySendDetails",
122            &params,
123            access_key_id,
124            access_secret,
125        )
126        .await?;
127        if gjson::get(&res, "Code").str() == "OK" {
128            let status =
129                gjson::get(&res, "SmsSendDetailDTOs.SmsSendDetailDTO.0.SendStatus").to_string();
130            let send_time = chrono::NaiveDateTime::parse_from_str(
131                &gjson::get(&res, "SmsSendDetailDTOs.SmsSendDetailDTO.0.SendDate").to_string(),
132                "%Y-%m-%d %H:%M:%S",
133            )
134            .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
135            .ok();
136            let receive_time = chrono::NaiveDateTime::parse_from_str(
137                &gjson::get(&res, "SmsSendDetailDTOs.SmsSendDetailDTO.0.ReceiveDate").to_string(),
138                "%Y-%m-%d %H:%M:%S",
139            )
140            .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
141            .ok();
142            // println!("{}", res);
143            return Ok(vec![SendDetailItem {
144                status: if status.as_str() == "2" {
145                    SendNotifyStatus::Failed
146                } else if status.as_str() == "3" {
147                    SendNotifyStatus::Completed
148                } else {
149                    SendNotifyStatus::Progress
150                },
151                message: gjson::get(&res, "SmsSendDetailDTOs.SmsSendDetailDTO.0.Content")
152                    .to_string(),
153                send_time,
154                receive_time,
155                code: gjson::get(&res, "SmsSendDetailDTOs.SmsSendDetailDTO.0.ErrCode").to_string(),
156                send_id: send_id.to_string(),
157                mobile: Some(mobile.to_string()),
158            }]);
159        }
160        Err(response_msg(&res, &["Message"]))
161    }
162    pub fn branch_limit() -> u16 {
163        100
164    }
165    #[allow(clippy::too_many_arguments)]
166    pub async fn branch_send(
167        client: Client,
168        region: &str, //"cn-hangzhou"
169        access_key_id: &str,
170        access_secret: &str,
171        sign_name: &str,
172        template_code: &str,
173        template_param: &str,
174        phone_numbers: &[&str],
175        out_id: &str,
176        ip: &str,
177    ) -> BranchSendResult {
178        let phone_numbers = phone_numbers_check(phone_numbers)?;
179        let mut params = HashMap::new();
180        let mobile = json!(phone_numbers).to_string();
181        if !region.is_empty() {
182            params.insert("RegionId", region);
183        }
184        let sign_name = json!(vec![sign_name; phone_numbers.len()]).to_string();
185        let mut param_data = vec![template_param; phone_numbers.len()].join(",");
186        param_data = format!("[{}]", param_data);
187
188        // {
189        //     "PhoneNumberJson": "[\"13800138000\",\"13800138001\"]",
190        //     "SignNameJson": "[\"ddddd\",\"ddddd\"]",
191        //     "TemplateCode": "SMS_35115133",
192        //     "TemplateParamJson": "[{\"code\":\"111\"},{\"code\":\"111\"}]",
193        //     "SourceIp": "218.18.137.26"
194        //   }
195        params.insert("Version", "2017-05-25");
196        params.insert("PhoneNumberJson", mobile.as_str());
197        params.insert("SignNameJson", sign_name.as_str());
198        params.insert("TemplateCode", template_code);
199        params.insert("TemplateParamJson", param_data.as_str());
200        params.insert("Action", "SendBatchSms");
201        if !out_id.is_empty() {
202            params.insert("OutId", out_id);
203        }
204
205        if !ip.is_empty() {
206            params.insert("SourceIp", ip);
207        }
208
209        let res = Self::build_request(
210            client,
211            Method::POST,
212            "dysmsapi.aliyuncs.com",
213            "/",
214            "",
215            "SendBatchSms",
216            &params,
217            access_key_id,
218            access_secret,
219        )
220        .await
221        .map_err(SendError::Next)?;
222
223        // println!("{}", res);
224
225        if gjson::get(&res, "Code").str() == "OK" {
226            return Ok(phone_numbers
227                .iter()
228                .map(|e| SendResultItem {
229                    mobile: e.to_string(),
230                    status: SendStatus::Progress,
231                    message: gjson::get(&res, "Message").to_string(),
232                    send_id: gjson::get(&res, "BizId").to_string(),
233                })
234                .collect());
235        }
236        Err(SendError::Next(response_msg(&res, &["Message"])))
237    }
238    #[allow(clippy::too_many_arguments)]
239    pub async fn build_request(
240        client: Client,
241        method: Method,
242        host: &str,
243        uri: &str,
244        query: &str,
245        action: &str,
246        params: &HashMap<&str, &str>,
247        secret_id: &str,
248        secret_key: &str,
249    ) -> Result<String, String> {
250        let now_time = now_time().unwrap_or_default();
251
252        let req_json =
253            serde_urlencoded::to_string(params).map_err(|e| format!("urlencoded fail:{}", e))?;
254
255        let mut hasher = Sha256::new();
256        hasher.update(&req_json);
257        let json_hash = format!("{:x}", hasher.finalize()).to_lowercase();
258
259        let datetime = NaiveDateTime::from_timestamp_opt(now_time as i64, 0).unwrap_or_default();
260
261        let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string();
262        let rand_s = rand_str(32);
263
264        let mut headers = HeaderMap::new();
265        if let Ok(value) = HeaderValue::from_str(host) {
266            headers.insert("Host", value);
267        }
268        if let Ok(value) = HeaderValue::from_str(action) {
269            headers.insert("x-acs-action", value);
270        }
271        if let Ok(value) = HeaderValue::from_str("2017-05-25") {
272            headers.insert("x-acs-version", value);
273        }
274        if let Ok(value) = HeaderValue::from_str(&datetime_str) {
275            headers.insert("x-acs-date", value);
276        }
277        if let Ok(value) = HeaderValue::from_str(&rand_s) {
278            headers.insert("x-acs-signature-nonce", value);
279        }
280        if let Ok(value) = HeaderValue::from_str(&json_hash) {
281            headers.insert("x-acs-content-sha256", value);
282        }
283
284        let sign_header_arr = &[
285            "host",
286            "x-acs-action",
287            "x-acs-content-sha256",
288            "x-acs-date",
289            "x-acs-signature-nonce",
290            "x-acs-version",
291        ];
292        let sign_header = sign_header_arr.join(";");
293        let mut my_header = Vec::with_capacity(sign_header_arr.len());
294        for tmp in sign_header_arr {
295            if *tmp == "host" {
296                my_header.push(format!("{}:{}", tmp, host))
297            } else if let Some(tval) = headers.get(*tmp) {
298                my_header.push(format!("{}:{}", tmp, tval.to_str().unwrap_or_default()))
299            }
300        }
301
302        let sign = format!(
303            "{}\n{}\n{}\n{}\n\n{}\n{}",
304            method.as_str(),
305            uri,
306            query,
307            my_header.join("\n"),
308            sign_header,
309            json_hash
310        );
311
312        debug!("post json:{}  header:{}", req_json, sign);
313
314        let mut hasher1 = Sha256::new();
315        hasher1.update(&sign);
316        let result = hasher1.finalize();
317
318        let string_to_sign = format!("ACS3-HMAC-SHA256\n{:x}", result);
319
320        // println!("SIGN:\n{}", string_to_sign);
321
322        debug!("sign body:{}", string_to_sign);
323
324        let mut mac = Hmac::<sha2::Sha256>::new_from_slice(secret_key.as_bytes())
325            .map_err(|e| format!("use data key on sha256 fail:{}", e))?;
326        mac.update(string_to_sign.as_bytes());
327        let signature = mac.finalize();
328
329        let data_sign = hex::encode(signature.into_bytes());
330        // ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=e521358f7776c97df52e6b2891a8bc73026794a071b50c3323388c4e0df64804
331        let authdata = format!(
332            "ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
333            secret_id, sign_header, data_sign
334        );
335
336        debug!(" authorization:{}", authdata);
337
338        if let Ok(value) = HeaderValue::from_str(&authdata) {
339            headers.insert("Authorization", value);
340        }
341        let url = format!("https://{}", host);
342
343        // println!(
344        //     "JSON-DATA:\n{}\n\n\nCanonicalRequest:\n{}\n\n\nStringToSign :\n{}\n\n\nAuthorization:\n{}\n\n\nheader:\n{:?}\n\n\nurl:\n{}",
345        //     req_json, sign, string_to_sign,authdata,headers, url
346        // );
347
348        let request = client.request(method, url).headers(headers).form(params);
349        let result = request
350            .send()
351            .await
352            .map_err(|e| format!("request send fail:{}", e))?;
353        let (_, res) = response_check(result, true).await?;
354        Ok(res)
355    }
356}