hi_push/huawei/
mod.rs

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    /*
47       自定义消息负载,通知栏消息支持JSON格式字符串,透传消息支持普通字符串或者JSON格式字符串。样例:"your data","{'param1':'value1','param2':'value2'}"。
48       消息体中有message.data,没有message.notification和message.android.notification,消息类型为透传消息。
49       如果用户发送的是网页应用的透传消息,那么接收消息中字段orignData为透传消息内容。
50    */
51    #[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>, // max: 1000 除开token,4096Bytes
59}
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    /*
78    0:对每个应用发送到该用户设备的离线消息只会缓存最新的一条。
79    -1:对所有离线消息都缓存。默认值是-1
80    1~100:离线消息缓存分组标识
81    */
82    pub collapse_key: Option<i64>,
83    pub urgency: Option<Urgency>,
84    pub category: Option<&'a str>,
85    //消息缓存时间,单位是秒 例如: 1000s 。 默认值为“86400s”(1天),最大值为“1296000s”(15天)。
86    pub ttl: Option<&'a str>,
87    // 批量任务消息标识,消息回执时会返回给应用服务器,应用服务器可以识别bi_tag对消息的下发情况进行统计分析。
88    pub bi_tag: Option<&'a str>,
89    pub receipt_id: Option<&'a str>,
90    /*
91       快应用发送透传消息时,指定小程序的模式类型,小程序有两种模式开发态和生产态,取值如下:
92       1:开发态
93       2:生产态
94       默认值是2。
95    */
96    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    /*
320        request and set token
321    */
322    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    /*
342        valid token before pushing
343    */
344    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}