hi_push/xiaomi/
mod.rs

1/**
2 * https://dev.mi.com/console/doc/detail?pId=2086
3 */
4use reqwest::header;
5use serde::{Deserialize, Serialize};
6use serde_repr::{Deserialize_repr, Serialize_repr};
7
8pub struct Config<'a> {
9    pub client_id: &'a str,
10    pub client_secret: &'a str,
11    pub project_id: &'a str,
12}
13
14pub struct Client {
15    cli: reqwest::Client,
16    client_secret: String,
17}
18
19#[derive(Debug, Serialize_repr, Deserialize_repr, Default, Clone)]
20#[repr(u8)]
21pub enum Passtrough {
22    Notice = 0,
23    #[default]
24    Transparent = 1,
25}
26
27#[derive(Debug, Serialize_repr, Deserialize_repr, Default, Clone)]
28#[repr(i8)]
29pub enum NotifyType {
30    All = -1,
31    #[default]
32    Quiet = 0,
33    // 静默推送
34    Sound = 1,
35    Vibrate = 2,
36    Lights = 4,
37}
38
39#[derive(Debug, Serialize, Default, Clone)]
40pub struct Message<'a> {
41    pub payload: Option<&'a str>,
42    // 消息的内容。(注意:需要对payload字符串做urlencode处理)
43    pub restricted_package_name: &'a str,
44    // App的包名。备注:V2版本支持一个包名,V3版本支持多包名(中间用逗号分割)。
45    pub pass_through: Passtrough,
46    pub title: &'a str,
47    // notify required <50
48    pub description: &'a str,
49    // notify required <128
50    pub notify_type: Option<&'a str>,
51    // notify_type的值可以是DEFAULT_ALL或者以下其他几种的OR组合:DEFAULT_ALL = -1;DEFAULT_SOUND = 1; // 使用默认提示音提示;DEFAULT_VIBRATE = 2; // 使用默认震动提示;DEFAULT_LIGHTS = 4; // 使用默认led灯光提示;
52    pub time_to_live: Option<&'a str>,
53    //可选项。如果用户离线,设置消息在服务器保存的时间,单位:ms。服务器默认最长保留两周。
54    pub time_to_send: Option<&'a str>,
55    //可选项。定时发送消息。用自1970年1月1日以来00:00:00.0 UTC时间表示(以毫秒为单位的时间)。注:仅支持七天内的定时消息。
56    pub notify_id: Option<i32>,
57    // 可选项。默认情况下,通知栏只显示一条推送消息。如果通知栏要显示多条推送消息,需要针对不同的消息设置不同的notify_id(相同notify_id的通知栏消息会覆盖之前的),且要求notify_id为取值在0~2147483647的整数。
58    pub registration_id: Option<String>,
59    // 根据registration_id,发送消息到指定设备上。可以提供多个registration_id,发送给一组设备,不同的registration_id之间用“,”分割。参数仅适用于“/message/regid”HTTP API。(注意:需要对payload字符串做urlencode处理)
60    pub alias: Option<&'a str>,
61    // 根据alias,发送消息到指定的设备。可以提供多个alias,发送给一组设备,不同的alias之间用“,”分割。参数仅适用于“/message/alias”HTTP API。
62    pub user_account: Option<&'a str>,
63    // 根据user_account,发送消息给设置了该user_account的所有设备。可以提供多个user_account,user_account之间用“,”分割。参数仅适用于“/message/user_account”HTTP API。
64    pub topic: Option<&'a str>,
65    // 根据topic,发送消息给订阅了该topic的所有设备。参数仅适用于“/message/topic”HTTP API。
66    pub topics: Option<&'a str>,
67    // topic列表,使用;$;分割。注: topics参数需要和topic_op参数配合使用,另外topic的数量不能超过5。参数仅适用于“/message/multi_topic”HTTP API。
68    pub topic_op: Option<&'a str>,
69    // topic之间的操作关系。支持以下三种:UNION并集INTERSECTION交集EXCEPT差集例如:topics的列表元素是[A, B, C, D],则并集结果是A∪B∪C∪D,交集的结果是A ∩B ∩C ∩D,差集的结果是A-B-C-D。参数仅适用于“/message/multi_topic”HTTP API。
70    #[serde(flatten)]
71    pub extra: Option<Extra<'a>>,
72}
73
74#[derive(Debug, Serialize_repr, Default, Clone)]
75#[repr(i16)]
76pub enum CallbackType {
77    #[default]
78    ArriveClick = 3,
79
80    Arrive = 1,
81    // 送达
82    Click = 2,
83    InvalidDevice = 16,
84    ClientForbidden = 32,
85}
86
87#[derive(Debug, Serialize_repr, Clone)]
88#[repr(u8)]
89pub enum NotifyEffect {
90    Unkown = 0,
91    Main = 1,
92    Intent = 2,
93    Web = 3,
94}
95
96#[derive(Debug, Serialize_repr, Clone)]
97#[repr(u8)]
98pub enum Bool {
99    False = 0,
100    True = 1,
101}
102
103impl From<Bool> for bool {
104    fn from(b: Bool) -> Self {
105        match b {
106            Bool::False => false,
107            Bool::True => true,
108        }
109    }
110}
111
112impl From<bool> for Bool {
113    fn from(b: bool) -> Self {
114        match b {
115            true => Self::True,
116            false => Self::False,
117        }
118    }
119}
120
121#[derive(Debug, Serialize, Default, Clone)]
122pub struct Extra<'a> {
123    #[serde(rename = "extra.sound_uri")]
124    pub sound_uri: Option<&'a str>,
125    // 可选项,自定义通知栏消息铃声。extra.sound_uri的值设置为铃声的URI。(请参见《服务端Java SDK文档》中的“自定义铃声”一节。)注:铃声文件放在Android app的raw目录下。
126    #[serde(rename = "extra.ticker")]
127    pub ticker: Option<&'a str>,
128    // 可选项,开启通知消息在状态栏滚动显示。
129    #[serde(rename = "extra.notify_foreground")]
130    pub notify_foreground: Option<Bool>,
131    // 可选项,开启/关闭app在前台时的通知弹出。当extra.notify_foreground值为”1″时,app会弹出通知栏消息;当extra.notify_foreground值为”0″时,app不会弹出通知栏消息。注:默认情况下会弹出通知栏消息。(请参见《服务端Java SDK文档》中的“控制App前台通知弹出”一节。)
132    #[serde(rename = "extra.notify_effect")]
133    pub notify_effect: Option<NotifyEffect>,
134    // 可选项,预定义通知栏消息的点击行为。通过设置extra.notify_effect的值以得到不同的预定义点击行为。“1″:通知栏点击后打开app的Launcher Activity。“2″:通知栏点击后打开app的任一Activity(开发者还需要传入extra.intent_uri)。“3″:通知栏点击后打开网页(开发者还需要传入extra.web_uri)。(请参见《服务端Java SDK文档》中的“预定义通知栏通知的点击行为”一节。)
135    #[serde(rename = "extra.intent_uri")]
136    pub intent_uri: Option<&'a str>,
137    // 可选项,打开当前app的任一组件。
138    #[serde(rename = "extra.web_uri")]
139    pub web_uri: Option<&'a str>,
140    // 可选项,打开某一个网页
141    #[serde(rename = "extra.flow_control")]
142    pub flow_control: Option<&'a str>,
143    // 可选项,控制是否需要进行平缓发送。默认不支持。value代表平滑推送的速度。注:服务端支持最低3000/s的qps。也就是说,如果将平滑推送设置为1000,那么真实的推送速度是3000/s。
144    #[serde(rename = "extra.caljob_keylback")]
145    pub job_key: Option<&'a str>,
146    // 可选项,使用推送批次(JobKey)功能聚合消息。客户端会按照jobkey去重,即相同jobkey的消息只展示第一条,其他的消息会被忽略。合法的jobkey由数字([0-9]),大小写字母([a-zA-Z]),下划线(_)和中划线(-)组成,长度不大于20个字符,且不能以下划线(_)开头。
147    #[serde(rename = "extra.only_send_once")]
148    pub only_send_once: Option<&'a str>,
149    //可选项,extra.only_send_once的值设置为1,表示该消息仅在设备在线时发送一次,不缓存离线消息进行多次下发。
150    #[serde(rename = "extra.callback")]
151    pub callback: Option<&'a str>,
152    #[serde(rename = "extra.callback.param")]
153    pub callback_param: Option<&'a str>,
154    #[serde(rename = "extra.callback.type")]
155    pub callback_type: Option<CallbackType>,
156}
157
158#[derive(Debug, Deserialize, Default)]
159pub struct Data {
160    pub id: String,
161}
162
163#[derive(Debug, Deserialize, Default)]
164#[serde(default)]
165pub struct Response {
166    pub result: String,
167    pub description: String,
168    pub data: Data,
169    pub code: i32,
170    pub info: String,
171    pub reason: String,
172}
173
174impl<'a> Client {
175    const TOKEN_LIMIT: i32 = 1000;
176
177    const PUSH_URL: &'static str = "https://api.xmpush.xiaomi.com/v4/message/regid";
178
179    pub fn new(conf: &'a Config<'a>) -> Result<Self, super::Error> {
180        let mut default_header = header::HeaderMap::new();
181
182        unsafe {
183            default_header.insert(
184                header::AUTHORIZATION,
185                header::HeaderValue::from_str(&format!("key={}", conf.client_secret))
186                    .unwrap_unchecked(), // @unsafe
187            );
188        }
189
190        let cli = reqwest::Client::builder()
191            .default_headers(default_header)
192            .build()
193            .map_err(|e| super::InnerError::Http(e.to_string()))?;
194
195        Ok(Client { cli, client_secret: conf.client_secret.to_string() })
196    }
197}
198
199#[async_trait::async_trait]
200impl<'b> super::Pusher<'b, Message<'b>, Response> for Client {
201    const TOKEN_LIMIT: usize = 1000;
202
203    async fn push(&self, msg: &'b Message) -> Result<Response, crate::Error> {
204        let resp = self
205            .cli
206            .post(Self::PUSH_URL)
207            .form(msg)
208            .send()
209            .await?
210            .error_for_status()?;
211
212        match resp.json::<Response>().await {
213            Ok(resp) => match resp.code {
214                0 => Ok(resp),
215                21301 => Err(super::RetryError::Auth(format!("{:?}", resp)).into()),
216                200001 => Err(super::InnerError::MessageLimit.into()),
217                200002 => Err(super::RetryError::QPS.into()),
218                _ => Err(super::InnerError::Response(format!("{:?}", resp)).into()),
219            },
220            Err(e) => Err(super::InnerError::Response(e.to_string()).into()),
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use crate::Pusher;
228
229    #[tokio::test]
230    async fn test_client() {
231        use super::*;
232
233        let client_id = std::env::var("MI_CLIENT_ID").unwrap();
234        let client_secret = std::env::var("MI_CLIENT_SECRET").unwrap();
235        let project_id = std::env::var("MI_PROJECT_ID").unwrap();
236
237        let cli = Client::new(&Config {
238            client_id: &client_id,
239            client_secret: &client_secret,
240            project_id: &project_id,
241        })
242            .unwrap();
243
244        let mut msg = Message::default();
245        msg.registration_id = Some("8whvC7gdG2QzNRZHUUPDQQ01laI9ZavQ/HbDTvEbHG2/XrY2Jj02nOAgZZx3T2Xw,12341".to_string());
246        msg.payload = Some("a=123");
247        msg.extra = Extra {
248            callback: "https://callback".into(),
249            notify_foreground: Bool::True.into(),
250            ..Default::default()
251        }
252            .into();
253        let resp = cli.push(&msg).await;
254
255
256        println!("{resp:?}");
257    }
258}