1use std::{
38 collections::BTreeMap,
39 convert::AsRef,
40 fmt::{self, Display, Formatter},
41};
42
43use chrono::{NaiveDate, Utc};
44use futures_util::TryFutureExt;
45use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer};
46use reqwest::Client;
47use serde::{Deserialize, Serialize};
48use serde_json::Value;
49use textnonce::TextNonce;
50use thiserror::Error;
51use url::Url;
52
53static MAX_PAGE_SIZE: u8 = 50;
54static REQUEST_FORMAT: &str = "JSON";
55static SIGN_METHOD: &str = "HMAC-SHA1";
56static SIGNATURE_VERSION: &str = "1.0";
57static VERSION: &str = "2017-05-25";
58
59#[derive(Debug, Error)]
60pub enum DayuError {
61 #[error("config of '{0}' absence")]
62 ConfigAbsence(&'static str),
63 #[error("dayu response error: {0}")]
64 Dayu(DayuFailResponse),
65 #[error("openssl error: {0}")]
66 Openssl(#[from] openssl::error::ErrorStack),
67 #[error("page size '{0}' too large, max is 50")]
68 PageTooLarge(u8),
69 #[error("reqwest error: {0}")]
70 Reqwest(#[from] reqwest::Error),
71 #[error("serde_json error: {0}")]
72 SerdeJson(#[from] serde_json::error::Error),
73 #[error("std io error: {0}")]
74 Stdio(#[from] std::io::Error),
75 #[error("textnonce error: {0}")]
76 TextNonce(String),
77 #[error("url parse error: {0}")]
78 UrlParse(#[from] url::ParseError),
79}
80
81#[derive(Debug, Deserialize)]
82#[serde(rename_all = "PascalCase")]
83pub struct DayuSendResponse {
84 pub biz_id: String,
85}
86
87#[derive(Debug, Deserialize)]
88#[serde(rename_all = "PascalCase")]
89pub struct DayuQueryDetail {
90 pub phone_num: String,
91 pub send_date: String,
92 pub send_status: u8,
93 pub receive_date: String,
94 pub template_code: String,
95 pub content: String,
96 pub err_code: String,
97}
98
99#[derive(Debug, Deserialize)]
100pub struct DayuQueryDetails {
101 #[serde(rename = "SmsSendDetailDTO")]
102 pub inner: Vec<DayuQueryDetail>,
103}
104
105#[derive(Debug, Deserialize)]
106#[serde(rename_all = "PascalCase")]
107pub struct DayuQueryResponse {
108 pub total_count: i32,
109 pub total_page: Option<u8>,
110 #[serde(rename = "SmsSendDetailDTOs")]
111 pub details: Option<DayuQueryDetails>,
112}
113
114#[derive(Debug, Deserialize, Serialize)]
115#[serde(rename_all = "PascalCase")]
116pub struct DayuFailResponse {
117 pub code: String,
118 pub message: String,
119 pub request_id: String,
120}
121
122impl Display for DayuFailResponse {
123 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
124 write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
125 }
126}
127
128#[derive(Debug, Deserialize)]
129#[serde(untagged)]
130pub enum DayuResponse {
131 Send(DayuSendResponse),
132 Query(DayuQueryResponse),
133 Fail(DayuFailResponse),
134}
135
136#[derive(Default, Clone)]
137pub struct Dayu {
138 client: Client,
139 access_key: String,
140 access_secret: String,
141 sign_name: String,
142}
143
144fn make_url(dayu: &Dayu, action: &str, params: &[(&str, &str)]) -> Result<Url, DayuError> {
145 if dayu.access_key.is_empty() {
146 return Err(DayuError::ConfigAbsence("access_key"));
147 }
148
149 if dayu.access_secret.is_empty() {
150 return Err(DayuError::ConfigAbsence("access_secret"));
151 }
152
153 if dayu.sign_name.is_empty() {
154 return Err(DayuError::ConfigAbsence("sign_name"));
155 }
156
157 let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
158
159 TextNonce::sized(32)
160 .map_err(DayuError::TextNonce)
161 .map(|v| v.to_string())
162 .and_then(|text_nonce| {
163 let mut map = BTreeMap::new();
164 map.insert("Format", REQUEST_FORMAT);
165 map.insert("AccessKeyId", &dayu.access_key);
166 map.insert("SignatureMethod", SIGN_METHOD);
167 map.insert("SignatureNonce", &text_nonce);
168 map.insert("SignatureVersion", SIGNATURE_VERSION);
169 map.insert("Timestamp", ×tamp);
170 map.insert("Action", action);
171 map.insert("SignName", &dayu.sign_name);
172 map.insert("Version", VERSION);
173
174 for &(name, value) in params {
175 if !value.is_empty() {
176 map.insert(name, value);
177 }
178 }
179
180 let mut forms = map
181 .into_iter()
182 .map(|(key, value)| (key, urlencoding::encode(value).into_owned()))
183 .collect::<Vec<(&str, String)>>();
184
185 let mut wait_sign = String::from("GET&%2F&");
186 wait_sign.push_str(
187 &forms
188 .iter()
189 .fold(vec![], |mut wait_sign, &(key, ref value)| {
190 wait_sign
191 .push(urlencoding::encode(&format!("{}={}", key, value)).into_owned());
192 wait_sign
193 })
194 .join(&urlencoding::encode("&")),
195 );
196
197 PKey::hmac(format!("{}&", &dayu.access_secret).as_bytes())
198 .and_then(|pkey| {
199 Signer::new(MessageDigest::sha1(), &pkey).and_then(|mut signer| {
200 signer
201 .update(wait_sign.as_bytes())
202 .and_then(|_| signer.sign_to_vec())
203 })
204 })
205 .map_err(Into::into)
206 .map(|ref signature| {
207 forms.push((
208 "Signature",
209 urlencoding::encode(&base64::encode(signature)).into_owned(),
210 ))
211 })
212 .and_then(|_| {
213 Url::parse("https://dysmsapi.aliyuncs.com")
214 .map_err(Into::into)
215 .map(|mut url| {
216 url.set_query(Some(
217 &forms
218 .into_iter()
219 .map(|(key, value)| format!("{}={}", key, value))
220 .collect::<Vec<String>>()
221 .join("&"),
222 ));
223 url
224 })
225 })
226 })
227}
228
229macro_rules! do_request {
230 ($dayu:expr, $action:expr, $params:expr, $type:tt) => {{
231 let url = make_url($dayu, $action, $params)?;
232 $dayu
233 .client
234 .get(url)
235 .send()
236 .and_then(|response| response.json::<DayuResponse>())
237 .await
238 .map_err(Into::into)
239 .and_then(|json_response| match json_response {
240 DayuResponse::$type(v) => Ok(v),
241 DayuResponse::Fail(fail) => Err(DayuError::Dayu(fail)),
242 _ => unreachable!(),
243 })
244 }};
245}
246
247impl Dayu {
248 pub fn new() -> Self {
250 Self::default()
251 }
252
253 pub fn set_access_key(mut self, access_key: impl Into<String>) -> Self {
255 self.access_key = access_key.into();
256 self
257 }
258
259 pub fn set_access_secret(mut self, access_secret: impl Into<String>) -> Self {
261 self.access_secret = access_secret.into();
262 self
263 }
264
265 pub fn set_sign_name(mut self, sign_name: impl Into<String>) -> Self {
267 self.sign_name = sign_name.into();
268 self
269 }
270
271 pub async fn sms_send<P: AsRef<str>, T: AsRef<str>>(
276 &self,
277 phones: &[P],
278 template_code: T,
279 template_param: Option<&Value>,
280 ) -> Result<DayuSendResponse, DayuError> {
281 let phone_numbers = phones
282 .iter()
283 .map(AsRef::as_ref)
284 .collect::<Vec<&str>>()
285 .join(",");
286
287 let template_param = template_param
288 .map(|v| serde_json::to_string(v).unwrap())
289 .unwrap_or_else(String::new);
290
291 do_request!(
292 self,
293 "SendSms",
294 &[
295 ("TemplateCode", template_code.as_ref()),
296 ("PhoneNumbers", &phone_numbers),
297 ("TemplateParam", &template_param),
298 ],
299 Send
300 )
301 }
302
303 pub async fn sms_query(
305 &self,
306 phone_number: &str,
307 biz_id: Option<&str>,
308 send_date: NaiveDate,
309 current_page: u8,
310 page_size: u8,
311 ) -> Result<DayuQueryResponse, DayuError> {
312 if page_size > MAX_PAGE_SIZE {
313 return Err(DayuError::PageTooLarge(page_size));
314 }
315
316 let send_date = send_date.format("%Y%m%d").to_string();
317 let page_size = page_size.to_string();
318 let current_page = current_page.to_string();
319
320 do_request!(
321 self,
322 "QuerySendDetails",
323 &[
324 ("PhoneNumber", phone_number),
325 ("BizId", biz_id.unwrap_or("")),
326 ("SendDate", &send_date),
327 ("PageSize", &page_size),
328 ("CurrentPage", ¤t_page),
329 ],
330 Query
331 )
332 }
333}