1use 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 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 pub restricted_package_name: &'a str,
44 pub pass_through: Passtrough,
46 pub title: &'a str,
47 pub description: &'a str,
49 pub notify_type: Option<&'a str>,
51 pub time_to_live: Option<&'a str>,
53 pub time_to_send: Option<&'a str>,
55 pub notify_id: Option<i32>,
57 pub registration_id: Option<String>,
59 pub alias: Option<&'a str>,
61 pub user_account: Option<&'a str>,
63 pub topic: Option<&'a str>,
65 pub topics: Option<&'a str>,
67 pub topic_op: Option<&'a str>,
69 #[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 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 #[serde(rename = "extra.ticker")]
127 pub ticker: Option<&'a str>,
128 #[serde(rename = "extra.notify_foreground")]
130 pub notify_foreground: Option<Bool>,
131 #[serde(rename = "extra.notify_effect")]
133 pub notify_effect: Option<NotifyEffect>,
134 #[serde(rename = "extra.intent_uri")]
136 pub intent_uri: Option<&'a str>,
137 #[serde(rename = "extra.web_uri")]
139 pub web_uri: Option<&'a str>,
140 #[serde(rename = "extra.flow_control")]
142 pub flow_control: Option<&'a str>,
143 #[serde(rename = "extra.caljob_keylback")]
145 pub job_key: Option<&'a str>,
146 #[serde(rename = "extra.only_send_once")]
148 pub only_send_once: Option<&'a str>,
149 #[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(), );
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}