1use std::{sync::Arc, time::Duration};
2use http::StatusCode;
3
4use serde::{Deserialize, Serialize};
5use serde_repr::Serialize_repr;
6
7use oauth2::TokenResponse;
8use oauth2::{
9 basic::{BasicClient, BasicErrorResponseType, BasicTokenType},
10 reqwest::async_http_client,
11 EmptyExtraTokenFields, RevocationErrorResponseType, StandardErrorResponse,
12 StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse,
13};
14use oauth2::{AuthUrl, ClientId, ClientSecret, TokenUrl};
15use tokio::sync::RwLock;
16use crate::InnerError;
17
18type Token = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
19
20type AuthClient = oauth2::Client<
21 StandardErrorResponse<BasicErrorResponseType>,
22 StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
23 BasicTokenType,
24 StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>,
25 StandardRevocableToken,
26 StandardErrorResponse<RevocationErrorResponseType>,
27>;
28
29pub struct Client {
30 client_id: String,
31 client_secret: String,
32 auth: AuthClient,
33 token: Arc<RwLock<Option<Token>>>,
34 cli: reqwest::Client,
35}
36
37#[derive(Default, Debug, Serialize, Clone)]
38pub struct Message<'a> {
39 pub validate_only: bool,
40 #[serde(borrow)]
41 pub message: InnerMessage<'a>,
42}
43
44#[derive(Default, Debug, Serialize, Clone)]
45pub struct InnerMessage<'a> {
46 #[serde(skip_serializing_if = "Option::is_none")]
52 pub data: Option<&'a str>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub notification: Option<Notification<'a>>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub android: Option<AndroidConfig<'a>>,
57 #[serde(borrow)]
58 pub token: Vec<&'a str>, }
60
61#[derive(Default, Debug, Serialize, Clone)]
62pub struct Notification<'a> {
63 pub title: &'a str,
64 pub body: &'a str,
65 pub image: Option<&'a str>,
66}
67
68#[derive(Debug, Serialize, Clone)]
69#[serde(rename_all = "UPPERCASE")]
70pub enum Urgency {
71 High,
72 Normal,
73}
74
75#[derive(Default, Debug, Serialize, Clone)]
76pub struct AndroidConfig<'a> {
77 pub collapse_key: Option<i64>,
83 pub urgency: Option<Urgency>,
84 pub category: Option<&'a str>,
85 pub ttl: Option<&'a str>,
87 pub bi_tag: Option<&'a str>,
89 pub receipt_id: Option<&'a str>,
90 pub fast_app_target: Option<i64>,
97 pub data: Option<&'a str>,
98 pub notification: Option<AndroidNotification<'a>>,
99}
100
101#[derive(Debug, Serialize_repr, Clone)]
102#[repr(u8)]
103pub enum Style {
104 Default = 0,
105 BigText = 1,
106 Inbox = 2,
107}
108
109#[derive(Debug, Serialize, Clone)]
110#[serde(rename_all = "UPPERCASE")]
111pub enum Importance {
112 Low,
113 Normal,
114}
115
116#[derive(Default, Debug, Serialize, Clone)]
117pub struct AndroidNotification<'a> {
118 pub image: Option<&'a str>,
119 pub icon: Option<&'a str>,
120 pub color: Option<&'a str>,
121 pub sound: Option<&'a str>,
122 pub default_sound: Option<bool>,
123 pub tag: Option<&'a str>,
124 pub importance: Option<Importance>,
125 pub click_action: ClickAction<'a>,
126 pub body_loc_key: Option<&'a str>,
127 pub body_loc_args: &'a [&'a str],
128 pub title_loc_key: Option<&'a str>,
129 pub title_loc_args: &'a [&'a str],
130 pub channel_id: Option<&'a str>,
131 pub notify_summary: Option<&'a str>,
132 pub style: Option<Style>,
133 pub big_title: Option<&'a str>,
134 pub big_body: Option<&'a str>,
135 pub notify_id: Option<i64>,
136 pub group: Option<&'a str>,
137 pub badge: Option<Badge<'a>>,
138 pub foreground_show: Option<bool>,
139 pub ticker: Option<&'a str>,
140 pub when: Option<&'a str>,
141 pub local_only: bool,
142 pub use_default_vibrate: bool,
143 pub use_default_light: bool,
144 pub visibility: Option<&'a str>,
145 pub vibrate_config: Vec<&'a str>,
146 pub light_settings: LightSettings<'a>,
147 pub auto_clear: Option<i8>,
148}
149
150#[derive(Debug, Serialize_repr, Default, Clone)]
151#[repr(u8)]
152pub enum ClickActionType {
153 Intent = 1,
154 Web = 2,
155 #[default]
156 Main = 3,
157}
158
159#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
160pub enum Code {
161 #[serde(rename = "80000000")]
162 Success,
163 #[serde(rename = "80000001")]
164 Common,
165 #[serde(rename = "80100000")]
166 PartFailedErr,
167 #[serde(rename = "80100001")]
168 ParameterError,
169 #[serde(rename = "80100002")]
170 TokenMustOne,
171 #[serde(rename = "80100003")]
172 MsgBodyError,
173 #[serde(rename = "80100004")]
174 TTLErr,
175 #[serde(rename = "80200001")]
176 AuthFailedErr,
177 #[serde(rename = "80200003")]
178 AuthTokenTimeoutErr,
179 #[serde(rename = "80300007")]
180 TokenInvalid,
181 Other(String),
182}
183
184#[derive(Default, Debug, Serialize, Clone)]
185pub struct ClickAction<'a> {
186 #[serde(rename = "type")]
187 pub type_field: ClickActionType,
188 pub intent: Option<&'a str>,
189 pub url: Option<&'a str>,
190 pub action: Option<&'a str>,
191}
192
193impl<'a> ClickAction<'a> {
194 pub fn new_intent(action: &'a str) -> Self {
195 Self {
196 type_field: ClickActionType::Intent,
197 action: Some(action),
198 ..Default::default()
199 }
200 }
201 pub fn new_web(url: &'a str) -> Self {
202 Self {
203 type_field: ClickActionType::Web,
204 url: Some(url),
205 ..Default::default()
206 }
207 }
208 pub fn new_main() -> Self {
209 Self {
210 type_field: ClickActionType::Main,
211 ..Default::default()
212 }
213 }
214}
215
216#[derive(Default, Debug, Serialize, Clone)]
217pub struct Badge<'a> {
218 pub add_num: Option<i64>,
219 pub class: &'a str,
220 pub set_num: Option<i64>,
221}
222
223#[derive(Default, Debug, Serialize, Clone)]
224pub struct LightSettings<'a> {
225 pub color: Color,
226 pub light_on_duration: &'a str,
227 pub light_off_duration: &'a str,
228}
229
230#[derive(Default, Debug, Serialize, Clone)]
231pub struct Color {
232 pub alpha: Option<i64>,
233 pub red: Option<i64>,
234 pub green: Option<i64>,
235 pub blue: Option<i64>,
236}
237
238#[derive(Debug, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct SendResponse {
241 pub msg: String,
242 pub code: Code,
243 pub request_id: String,
244}
245
246#[derive(Debug)]
247pub struct Response {
248 pub msg: String,
249 pub code: Code,
250 pub request_id: String,
251 pub success: i64,
252 pub failure: i64,
253 pub illegal_tokens: Vec<String>,
254}
255
256#[derive(Debug, Deserialize)]
257pub struct InvalidMsg {
258 pub success: i64,
259 pub failure: i64,
260 pub illegal_tokens: Vec<String>,
261}
262
263impl SendResponse {
264 pub fn get_invalid_tokens(&self) -> Option<InvalidMsg> {
265 serde_json::from_str::<InvalidMsg>(self.msg.as_str())
266 .ok()
267 }
268
269 pub fn is_part_failed_err(&self) -> bool {
270 self.code == Code::PartFailedErr
271 }
272
273 pub fn take_invalid_tokens(&self) -> (Option<InvalidMsg>, bool) {
274 (
275 serde_json::from_str::<InvalidMsg>(self.msg.as_str()).ok(),
276 self.is_part_failed_err()
277 )
278 }
279}
280
281impl Client {
282 const TOKEN_URL: &'static str = "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
283 const PUSH_URL: &'static str = "https://push-api.cloud.huawei.com/v2/{}/messages:send";
284
285 pub async fn new(
286 client_id: &str,
287 client_secret: &str,
288 ) -> Result<Client, super::Error> {
289 let auth = BasicClient::new(
290 ClientId::new(client_id.to_string()),
291 Some(ClientSecret::new(client_secret.to_string())),
292 AuthUrl::new(Self::TOKEN_URL.to_string())
293 .map_err(|e| super::RetryError::Auth(e.to_string()))?,
294 Some(
295 TokenUrl::new(Self::TOKEN_URL.to_string())
296 .map_err(|e| super::RetryError::Auth(e.to_string()))?,
297 ),
298 );
299
300 let auth = auth.set_auth_type(oauth2::AuthType::RequestBody);
301
302 let cli = reqwest::Client::builder()
303 .build()
304 .map_err(|e| super::InnerError::Http(e.to_string()))?;
305
306 let res = Client {
307 auth,
308 client_id: client_id.to_string(),
309 client_secret: client_secret.to_string(),
310 token: Default::default(),
311 cli,
312 };
313
314 res.request_token().await?;
315
316 Ok(res)
317 }
318
319 async fn request_token(&self) -> Result<Token, super::Error> {
323 let token = self
324 .auth
325 .exchange_client_credentials()
326 .request_async(async_http_client)
327 .await
328 .map_err(|e| super::RetryError::Auth(e.to_string()))?;
329 self.set_token(token.clone()).await;
330
331 Ok(token)
332 }
333
334 async fn set_token(&self, mut token: Token) {
335 let expires_in =
336 chrono::Utc::now().timestamp() as u64 + (token.expires_in().unwrap().as_secs());
337 token.set_expires_in(Some(&Duration::from_secs(expires_in)));
338 *(self.token.write().await) = Some(token);
339 }
340
341 fn valid_token(&self, token: &Token) -> bool {
345 let expires = token.expires_in();
346 if expires.is_none() {
347 return false;
348 }
349 if expires.unwrap().as_secs() <= chrono::Utc::now().timestamp() as u64 {
350 return false;
351 }
352 true
353 }
354
355 #[inline]
356 fn build_push_url(&self) -> String {
357 format!("https://push-api.cloud.huawei.com/v1/{}/messages:send", self.client_id)
358 }
359}
360
361#[async_trait::async_trait]
362impl<'b> super::Pusher<'b, Message<'b>, Response> for Client {
363 async fn push(&self, msg: &'b Message) -> Result<Response, crate::Error> {
364 let token = self.token.clone();
365
366 let token = token.read().await;
367
368 let token = match token.clone() {
369 Some(token) => token.clone(),
370 None => match self.request_token().await {
371 Ok(token) => token,
372 Err(e) => return Err(super::RetryError::Auth(e.to_string()).into()),
373 },
374 };
375
376 if !self.valid_token(&token) {
377 return Err(super::RetryError::Auth("token expired or invalid".to_string()).into());
378 }
379
380 let resp = self
381 .cli
382 .post(self.build_push_url())
383 .bearer_auth(token.access_token().secret())
384 .json(msg)
385 .send()
386 .await?;
387
388 let status = resp.status();
389
390 match status {
391 StatusCode::OK | StatusCode::BAD_REQUEST => {
392 let resp = resp.json::<SendResponse>().await?;
393 let invalid = resp.get_invalid_tokens();
394
395 let mut res = Response {
396 msg: resp.msg.clone(),
397 code: resp.code.clone(),
398 request_id: resp.request_id.clone(),
399 success: 0,
400 failure: 0,
401 illegal_tokens: vec![],
402 };
403 match resp.code {
404 Code::Success => {}
405 Code::PartFailedErr => {
406 res.success = invalid.as_ref().map_or(msg.message.token.len() as i64, |e| e.success);
407 res.failure = invalid.as_ref().map_or(0, |e| e.failure);
408 res.illegal_tokens = invalid.map_or(Default::default(), |e| e.illegal_tokens);
409 }
410 Code::ParameterError
411 | Code::TokenMustOne
412 | Code::MsgBodyError
413 | Code::TTLErr => {
414 return Err(super::InnerError::InvalidParams(resp.msg).into());
415 }
416 Code::AuthFailedErr | Code::AuthTokenTimeoutErr => {
417 return Err(super::RetryError::Auth(resp.msg).into());
418 }
419 Code::TokenInvalid => {
420 res.failure = msg.message.token.len() as i64;
421 res.illegal_tokens = msg.message.token.iter().map(|e| e.to_string()).collect();
422 }
423 Code::Other(_) | Code::Common => {
424 return Err(super::InnerError::Unknown(format!("{:?}", resp)).into());
425 }
426 }
427 Ok(res)
428 }
429 _ => match resp.error_for_status() {
430 Ok(_) => unreachable!(""),
431 Err(e) => Err(e)?
432 }
433 }
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use crate::Pusher;
440
441 #[tokio::test]
442 async fn test_push() {
443 use super::*;
444
445 let client_id = std::env::var("HW_CLIENT_ID").unwrap();
446 let client_secret = std::env::var("HW_CLIENT_SECRET").unwrap();
447
448 let hw = Client::new(
449 &client_id,
450 &client_secret,
451 )
452 .await
453 .unwrap();
454 let msg = Message {
455 validate_only: false,
456 message: InnerMessage {
457 data: Some("hello"),
458 notification: None,
459 android: Some(AndroidConfig {
460 ..Default::default()
461 }),
462 token: vec![
463 "IQAAAACy0kYwAADWsJ-W5yOcL9booZrr1XdycVGvPWwWVrBG3AR838oq8gHM26Od6g_cxkQO_U1NbR720haQQ3VapXWyDMZyYj-MrSJeqUoq5k79Lw",
464 "1IQAAAACy0kYwAADWsJ-W5yOcL9booZrr1XdycVGvPWwWVrBG3AR838oq8gHM26Od6g_cxkQO_U1NbR720haQQ3VapXWyDMZyYj-MrSJeqUoq5k79Lw",
465 ],
466 },
467 };
468 let resp = hw.push(&msg).await;
469
470 println!("{resp:?}");
471 }
472}