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 pub fn send_notify_parse(notify_data: &str) -> BranchSendNotifyResult {
45 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 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 #[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 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}