Skip to main content

lsys_lib_sms/sms_lib/
sender_tencent.rs

1use chrono::{DateTime, NaiveDateTime, Utc};
2
3use hmac::{Hmac, Mac};
4use reqwest::{
5    header::{HeaderMap, HeaderValue},
6    Method,
7};
8use reqwest::{Client, StatusCode};
9use serde_json::json;
10use sha2::{Digest, Sha256};
11
12use crate::SendNotifyStatus;
13use crate::{
14    response_check, response_msg, sms_lib::phone_numbers_check, BranchSendDetailResult,
15    BranchSendNotifyResult, SendDetailItem, SendNotifyItem, SendResultItem, SendStatus,
16};
17use tracing::{debug, warn};
18
19use super::SendError;
20
21use super::{now_time, BranchSendResult};
22
23pub struct TenSms {}
24
25impl TenSms {
26    pub fn send_notify_output(res: &Result<(), String>) -> String {
27        match res {
28            Ok(_) => {
29                json!({
30                    "result": 0,
31                    "errmsg": "OK"
32                })
33            }
34            Err(err) => {
35                json!({
36                    "result" : 500,
37                    "errmsg" : err
38                })
39            }
40        }
41        .to_string()
42    }
43    //post json
44    pub fn send_notify_parse(notify_data: &str) -> BranchSendNotifyResult {
45        // [
46        //     {
47        //         "user_receive_time": "2015-10-17 08:03:04",
48        //         "nationcode": "86",
49        //         "mobile": "13xxxxxxxxx",
50        //         "report_status": "SUCCESS",
51        //         "errmsg": "DELIVRD",
52        //         "description": "用户短信送达成功",
53        //         "sid": "xxxxxxx"
54        //     }
55        // ]
56        let items = gjson::parse(notify_data);
57        let mut out = Vec::with_capacity(items.array().len());
58        for tmp in items.array() {
59            let receive_time = chrono::NaiveDateTime::parse_from_str(
60                &tmp.get("user_receive_time").to_string(),
61                "%Y-%m-%d %H:%M:%S",
62            )
63            .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
64            .ok();
65            out.push(SendNotifyItem {
66                status: if tmp.get("report_status").str() == "SUCCESS" {
67                    SendNotifyStatus::Completed
68                } else {
69                    SendNotifyStatus::Failed
70                },
71                message: tmp.get("description").to_string(),
72                send_time: None,
73                receive_time,
74                code: tmp.get("errmsg").to_string(),
75                send_id: tmp.get("sid").to_string(),
76                mobile: Some(tmp.get("mobile").to_string()),
77            });
78        }
79        Ok(out)
80    }
81
82    pub async fn send_detail(
83        client: Client,
84        region: &str,
85        secret_id: &str,
86        secret_key: &str,
87        sms_app_id: &str,
88        mobile: &str,
89        send_date: &str,
90    ) -> BranchSendDetailResult {
91        let start_time = chrono::NaiveDateTime::parse_from_str(
92            &format!("{} 00:00:00", send_date),
93            "%Y-%m-%d %H:%M:%S",
94        )
95        .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
96        .unwrap_or(now_time().unwrap_or_default() - 3600 * 24);
97
98        let end_time = chrono::NaiveDateTime::parse_from_str(
99            &format!("{} 23:59:59", send_date),
100            "%Y-%m-%d %H:%M:%S",
101        )
102        .map(|tmp| DateTime::<Utc>::from_naive_utc_and_offset(tmp, Utc).timestamp() as u64)
103        .unwrap_or(now_time().unwrap_or_default() - 3600 * 24);
104
105        let reqjson = json!({
106            "PhoneNumber": mobile,
107            "SmsSdkAppId": sms_app_id,
108            "BeginTime": start_time,
109            "Offset": 0,
110            "Limit": 100,
111            "EndTime":end_time,
112        });
113
114        let res = Self::build_request(
115            client,
116            "sms.tencentcloudapi.com",
117            Method::POST,
118            "sms",
119            region,
120            "PullSmsSendStatusByPhoneNumber",
121            &reqjson.to_string(),
122            secret_id,
123            secret_key,
124        )
125        .await?;
126        let code = gjson::get(&res, "Response.PullSmsSendStatusSet");
127        if code.exists() {
128            let mut out = Vec::with_capacity(code.array().len());
129            for tmp in code.array() {
130                // {
131                //     "Description": "DELIVRD",
132                //     "CountryCode": "86",
133                //     "SubscriberNumber": "15291996666",
134                //     "ReportStatus": "SUCCESS",
135                //     "PhoneNumber": "+8615291996666",
136                //     "SerialNo": "14:19325917feb3914eb78b50d6182d7e452e",
137                //     "UserReceiveTime": 1620734188
138                // },
139                out.push(SendDetailItem {
140                    status: if tmp.get("ReportStatus").str() == "SUCCESS" {
141                        SendNotifyStatus::Completed
142                    } else if tmp.get("ReportStatus").str() == "FAIL" {
143                        SendNotifyStatus::Failed
144                    } else {
145                        SendNotifyStatus::Progress
146                    },
147                    message: tmp.get("Description").to_string(),
148                    send_time: None,
149                    receive_time: Some(tmp.get("UserReceiveTime").u64()),
150                    code: gjson::get(&res, "SmsSendDetailDTOs.0.ErrCode").to_string(),
151                    send_id: tmp.get("SerialNo").to_string(),
152                    mobile: Some(tmp.get("PhoneNumber").to_string()),
153                });
154            }
155            return Ok(out);
156        }
157        Err(response_msg(&res, &["Response.Error.Message"]))
158    }
159    pub fn branch_limit() -> u16 {
160        200
161    }
162    //执行短信发送
163    #[allow(clippy::too_many_arguments)]
164    pub async fn branch_send(
165        client: Client,
166        region: &str,
167        secret_id: &str,
168        secret_key: &str,
169        sms_app_id: &str,
170        sign_name: &str,
171        template_id: &str,
172        template_arr: Option<Vec<String>>,
173        phone_numbers: &[&str],
174    ) -> BranchSendResult {
175        let phone_numbers = phone_numbers_check(phone_numbers)?;
176        let mut reqjson = json!({
177            "PhoneNumberSet": phone_numbers,
178            "SmsSdkAppId": sms_app_id,
179            "SignName": sign_name,
180            "TemplateId": template_id,
181        });
182        if let Some(data_var) = template_arr {
183            reqjson["TemplateParamSet"] = data_var.into();
184        }
185
186        let res = Self::build_request(
187            client,
188            "sms.tencentcloudapi.com",
189            Method::POST,
190            "sms",
191            region,
192            "SendSms",
193            &reqjson.to_string(),
194            secret_id,
195            secret_key,
196        )
197        .await
198        .map_err(SendError::Next)?;
199        let code = gjson::get(&res, "Response.SendStatusSet");
200        if code.exists() {
201            let mut out = Vec::with_capacity(code.array().len());
202            for tmp in code.array() {
203                out.push(SendResultItem {
204                    mobile: tmp.get("PhoneNumber").to_string(),
205                    status: if tmp.get("Code").str() == "Ok" {
206                        SendStatus::Progress
207                    } else {
208                        SendStatus::Failed(true)
209                    },
210                    message: tmp.get("Message").to_string(),
211                    send_id: tmp.get("SerialNo").to_string(),
212                })
213            }
214            return Ok(out);
215        }
216        Err(SendError::Next(response_msg(
217            &res,
218            &["Response.Error.Message"],
219        )))
220    }
221    #[allow(clippy::too_many_arguments)]
222    pub async fn build_request(
223        client: Client,
224        host: &str,
225        method: Method,
226        service: &str,
227        region: &str,
228        action: &str,
229        req_json: &str,
230        secret_id: &str,
231        secret_key: &str,
232    ) -> Result<String, String> {
233        let now_time = now_time().unwrap_or_default();
234        let mut headers = HeaderMap::new();
235        if let Ok(value) = HeaderValue::from_str(host) {
236            headers.insert("Host", value);
237        }
238        if let Ok(value) = HeaderValue::from_str(action) {
239            headers.insert("X-TC-Action", value);
240        }
241        if let Ok(value) = HeaderValue::from_str("2021-01-11") {
242            headers.insert("X-TC-Version", value);
243        }
244        if let Ok(value) = HeaderValue::from_str(&now_time.to_string()) {
245            headers.insert("X-TC-Timestamp", value);
246        }
247        if let Ok(value) = HeaderValue::from_str("zh-CN") {
248            headers.insert("X-TC-Language", value);
249        }
250        if let Ok(value) = HeaderValue::from_str(region) {
251            headers.insert("X-TC-Region", value);
252        }
253        if let Ok(value) = HeaderValue::from_str("application/json") {
254            headers.insert("Content-Type", value);
255        }
256        let datetime = NaiveDateTime::from_timestamp_opt(now_time as i64, 0).unwrap_or_default();
257
258        let reqjson = req_json.to_string();
259
260        let mut hasher = Sha256::new();
261        hasher.update(&reqjson);
262        let result = hasher.finalize();
263
264        let sign = format!(
265             "{}\n/\n\ncontent-type:application/json\nhost:{}\nx-tc-action:{}\n\ncontent-type;host;x-tc-action\n{}",
266             method.as_str(),
267             host,
268             action.to_lowercase(),
269             format!("{:x}", result).to_lowercase()
270         );
271
272        let now_date = datetime.format("%Y-%m-%d").to_string();
273        debug!("tencent post json:{}  header:{}", reqjson, sign);
274
275        let mut hasher1 = Sha256::new();
276        hasher1.update(&sign);
277        let result = hasher1.finalize();
278
279        let string_to_sign = format!(
280            "TC3-HMAC-SHA256\n{}\n{}/{}/tc3_request\n{}",
281            now_time,
282            now_date,
283            service,
284            format!("{:x}", result).to_lowercase()
285        );
286
287        debug!("sign body:{}", string_to_sign);
288
289        let mut mac = Hmac::<sha2::Sha256>::new_from_slice(format!("TC3{}", secret_key).as_bytes())
290            .map_err(|e| format!("use data key on sha256 fail:{}", e))?;
291        mac.update(now_date.as_bytes());
292        let result = mac.finalize().into_bytes();
293
294        let mut mac1 = Hmac::<sha2::Sha256>::new_from_slice(&result)
295            .map_err(|e| format!("use on sha256 fail:{}", e))?;
296        mac1.update(service.as_bytes());
297        let result = mac1.finalize();
298
299        let mut mac2 = Hmac::<sha2::Sha256>::new_from_slice(&result.into_bytes())
300            .map_err(|e| format!("use tc3_request on sha256 fail:{}", e))?;
301        mac2.update(b"tc3_request");
302        let secret_signing = mac2.finalize();
303
304        let mut tmp3 = Hmac::<sha2::Sha256>::new_from_slice(&secret_signing.into_bytes())
305            .map_err(|e| format!("use tc3_request on sha256 fail:{}", e))?;
306        tmp3.update(string_to_sign.as_bytes());
307        let signature = tmp3.finalize();
308
309        let data_sign = hex::encode(signature.into_bytes());
310
311        let authdata =format!(
312                     "TC3-HMAC-SHA256 Credential={}/{}/{}/tc3_request, SignedHeaders=content-type;host;x-tc-action, Signature={}",
313                     secret_id,
314                     datetime.format("%Y-%m-%d"),
315                     service,
316                     data_sign
317                 );
318
319        // println!("{}\n{}", authdata, string_to_sign);
320
321        if let Ok(value) = HeaderValue::from_str(&authdata) {
322            headers.insert("Authorization", value);
323        }
324
325        let request = client
326            .request(method, format!("https://{}/", host))
327            .headers(headers)
328            .body(reqjson);
329        let result = request
330            .send()
331            .await
332            .map_err(|e| format!("Tenyun request send fail:{}", e))?;
333        let (status, res) = response_check(result, true).await?;
334        if status != StatusCode::OK {
335            warn!("response fail: {}", &res);
336            return Err(format!("http bad:{}", res));
337        }
338        Ok(res)
339    }
340}