1use std::collections::HashMap;
2
3use reqwest::StatusCode;
4use serde::{Deserialize, Serialize};
5use serde_repr::{Deserialize_repr, Serialize_repr};
6use uuid::Uuid;
7
8#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
10#[serde(rename_all = "PascalCase")]
11pub enum ApiErrorReason {
12 BadCollapseId,
13 BadDeviceToken,
14 BadExpirationDate,
15 BadMessageId,
16 BadPriority,
17 BadTopic,
18 DeviceTokenNotForTopic,
19 DuplicateHeaders,
20 IdleTimeout,
21 MissingDeviceToken,
22 MissingTopic,
23 PayloadEmpty,
24 TopicDisallowed,
25 BadCertificate,
26 BadCertificateEnvironment,
27 ExpiredProviderToken,
28 Forbidden,
29 InvalidProviderToken,
30 MissingProviderToken,
31 BadPath,
32 MethodNotAllowed,
33 Unregistered,
34 PayloadTooLarge,
35 TooManyProviderTokenUpdates,
36 TooManyRequests,
37 InternalServerError,
38 ServiceUnavailable,
39 Shutdown,
40 Other(String),
41}
42
43impl ApiErrorReason {
44 fn to_str(&self) -> &str {
45 use self::ApiErrorReason::*;
46 match self {
47 &BadCollapseId => "BadCollapseId",
48 &BadDeviceToken => "BadDeviceToken",
49 &BadExpirationDate => "BadExpirationDate",
50 &BadMessageId => "BadMessageId",
51 &BadPriority => "BadPriority",
52 &BadTopic => "BadTopic",
53 &DeviceTokenNotForTopic => "DeviceTokenNotForTopic",
54 &DuplicateHeaders => "DuplicateHeaders",
55 &IdleTimeout => "IdleTimeout",
56 &MissingDeviceToken => "MissingDeviceToken",
57 &MissingTopic => "MissingTopic",
58 &PayloadEmpty => "PayloadEmpty",
59 &TopicDisallowed => "TopicDisallowed",
60 &BadCertificate => "BadCertificate",
61 &BadCertificateEnvironment => "BadCertificateEnvironment",
62 &ExpiredProviderToken => "ExpiredProviderToken",
63 &Forbidden => "Forbidden",
64 &InvalidProviderToken => "InvalidProviderToken",
65 &MissingProviderToken => "MissingProviderToken",
66 &BadPath => "BadPath",
67 &MethodNotAllowed => "MethodNotAllowed",
68 &Unregistered => "Unregistered",
69 &PayloadTooLarge => "PayloadTooLarge",
70 &TooManyProviderTokenUpdates => "TooManyProviderTokenUpdates",
71 &TooManyRequests => "TooManyRequests",
72 &InternalServerError => "InternalServerError",
73 &ServiceUnavailable => "ServiceUnavailable",
74 &Shutdown => "Shutdown",
75 &Other(ref val) => val,
76 }
77 }
78}
79
80pub static APN_URL_PRODUCTION: &'static str = "https://api.push.apple.com";
82
83pub static APN_URL_DEV: &'static str = "https://api.sandbox.push.apple.com";
85
86#[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy, Debug)]
89#[repr(u8)]
90pub enum Priority {
91 Low = 1,
92 Middle = 5,
94 High = 10, }
96
97impl Priority {
98 pub fn to_uint(self) -> u8 {
100 self as u8
101 }
102}
103
104#[derive(Debug)]
105pub struct CollapseIdTooLongError;
106
107#[derive(Serialize, Clone, Debug)]
110pub struct CollapseId<'a>(&'a str);
111
112impl<'a> CollapseId<'a> {
113 pub fn new(value: &'a str) -> Result<Self, CollapseIdTooLongError> {
116 if value.len() > 64 {
118 Err(CollapseIdTooLongError)
119 } else {
120 Ok(CollapseId(value))
121 }
122 }
123
124 pub fn as_str(&self) -> &str {
126 &self.0
127 }
128}
129
130#[derive(Serialize, Debug, Clone)]
131#[serde(rename_all = "lowercase")]
132pub enum ApnsPushType {
133 Alert,
134 Background,
135 Voip,
136 Location,
137 Complication,
138 Fileprovider,
139 Mdm,
140}
141
142impl ApnsPushType {
143 pub fn as_str(&self) -> &'static str {
144 match self {
145 ApnsPushType::Alert => "alert",
146 ApnsPushType::Background => "background",
147 ApnsPushType::Voip => "voip",
148 ApnsPushType::Location => "location",
149 ApnsPushType::Complication => "complication",
150 ApnsPushType::Fileprovider => "fileprovider",
151 ApnsPushType::Mdm => "mdm",
152 }
153 }
154}
155
156#[derive(Serialize, Default, Clone, Debug)]
161pub struct AlertPayload<'a> {
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub title: Option<&'a str>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub body: Option<&'a str>,
166 #[serde(rename = "title-loc-key", skip_serializing_if = "Option::is_none")]
167 pub title_loc_key: Option<&'a str>,
168 #[serde(rename = "title-loc-args", skip_serializing_if = "Option::is_none")]
169 pub title_loc_args: Option<Vec<String>>,
170 #[serde(rename = "action-loc-key", skip_serializing_if = "Option::is_none")]
171 pub action_loc_key: Option<&'a str>,
172 #[serde(rename = "loc-key", skip_serializing_if = "Option::is_none")]
173 pub loc_key: Option<&'a str>,
174 #[serde(rename = "loc-args", skip_serializing_if = "Option::is_none")]
175 pub loc_args: Option<Vec<String>>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub loc_image: Option<&'a str>,
178}
179
180impl<'a> AlertPayload<'a> {
181 fn new(title: Option<&'a str>, body: Option<&'a str>) -> Self {
182 AlertPayload {
183 title: title,
184 body: body,
185 title_loc_key: None,
186 title_loc_args: None,
187 action_loc_key: None,
188 loc_key: None,
189 loc_args: None,
190 loc_image: None,
191 }
192 }
193}
194
195#[derive(Serialize, Clone, Debug)]
199#[serde(untagged)]
200pub enum Alert<'a> {
201 Simple(&'a str),
202 Payload(AlertPayload<'a>),
203}
204
205#[derive(Serialize, Default, Clone, Debug)]
206pub struct Payload<'a> {
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub alert: Option<Alert<'a>>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub badge: Option<u32>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub sound: Option<&'a str>,
215 #[serde(rename = "content-available", skip_serializing_if = "Option::is_none")]
217 pub content_available: Option<bool>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub category: Option<&'a str>,
220 #[serde(rename = "thread-id", skip_serializing_if = "Option::is_none")]
221 pub thread_id: Option<&'a str>,
222}
223
224#[derive(Serialize, Clone, Debug)]
226pub(crate) struct ApnsRequest<'a> {
227 pub aps: &'a Payload<'a>,
228 #[serde(flatten)]
229 pub custom: Option<&'a HashMap<&'a str, &'a str>>,
230}
231
232#[derive(Serialize, Clone, Debug)]
237pub struct Notification<'a> {
238 pub topic: &'a str,
240 pub device_token: &'a str,
241 pub payload: Payload<'a>,
242
243 pub id: Option<Uuid>,
245 pub expiration: Option<u64>,
247 pub priority: Option<Priority>,
249 pub collapse_id: Option<CollapseId<'a>>,
250 pub apns_push_type: Option<ApnsPushType>,
251 pub custom: Option<HashMap<&'a str, &'a str>>,
252}
253
254impl<'a> Notification<'a> {
255 pub fn new(topic: &'a str, device_token: &'a str, payload: Payload<'a>) -> Self {
257 Notification {
258 topic,
259 device_token,
260 payload,
261 id: None,
262 expiration: None,
263 priority: None,
264 collapse_id: None,
265 apns_push_type: None,
266 custom: None,
267 }
268 }
269}
270
271pub struct NotificationBuilder<'a> {
273 notification: Notification<'a>,
274}
275
276impl<'a> NotificationBuilder<'a> {
277 pub fn new(topic: &'a str, device_id: &'a str) -> Self {
278 NotificationBuilder {
279 notification: Notification::new(topic, device_id, Payload::default()),
280 }
281 }
282
283 pub fn push_type(&mut self, push_type: ApnsPushType) -> &mut Self {
284 self.notification.apns_push_type = push_type.into();
285 self
286 }
287
288 pub fn payload(&mut self, payload: Payload<'a>) -> &mut Self {
289 self.notification.payload = payload;
290 self
291 }
292
293 pub fn alert(&mut self, alert: &'a str) -> &mut Self {
294 self.notification.payload.alert = Some(Alert::Simple(alert.into()));
295 self
296 }
297
298 pub fn title(&mut self, title: &'a str) -> &mut Self {
299 let payload = match self.notification.payload.alert.take() {
300 None => AlertPayload::new(Some(title), None),
301 Some(Alert::Simple(_)) => AlertPayload::new(Some(title), None),
302 Some(Alert::Payload(mut payload)) => {
303 payload.title = Some(title);
304 payload
305 }
306 };
307 self.notification.payload.alert = Some(Alert::Payload(payload));
308 self
309 }
310
311 pub fn body(&mut self, body: &'a str) -> &mut Self {
312 let payload = match self.notification.payload.alert.take() {
313 None => AlertPayload::new(None, Some(body)),
314 Some(Alert::Simple(title)) => AlertPayload::new(Some(title), Some(body)),
315 Some(Alert::Payload(mut payload)) => {
316 payload.body = Some(body);
317 payload
318 }
319 };
320 self.notification.payload.alert = Some(Alert::Payload(payload));
321 self
322 }
323
324 pub fn badge(&mut self, number: u32) -> &mut Self {
325 self.notification.payload.badge = Some(number);
326 self
327 }
328
329 pub fn sound(&mut self, sound: &'a str) -> &mut Self {
330 self.notification.payload.sound = Some(sound.into());
331 self
332 }
333
334 pub fn content_available(&mut self) -> &mut Self {
335 self.notification.payload.content_available = Some(true);
336 self
337 }
338
339 pub fn category(&mut self, category: &'a str) -> &mut Self {
340 self.notification.payload.category = Some(category);
341 self
342 }
343
344 pub fn thread_id(&mut self, thread_id: &'a str) -> &mut Self {
345 self.notification.payload.thread_id = Some(thread_id);
346 self
347 }
348
349 pub fn id(&mut self, id: Uuid) -> &mut Self {
350 self.notification.id = Some(id);
351 self
352 }
353
354 pub fn expiration(&mut self, expiration: u64) -> &mut Self {
355 self.notification.expiration = Some(expiration);
356 self
357 }
358
359 pub fn priority(&mut self, priority: Priority) -> &mut Self {
360 self.notification.priority = Some(priority);
361 self
362 }
363
364 pub fn collapse_id(&mut self, id: CollapseId<'a>) -> &mut Self {
365 self.notification.collapse_id = Some(id);
366 self
367 }
368
369 pub fn custom(&mut self, custom: HashMap<&'a str, &'a str>) -> &mut Self {
370 self.notification.custom = Some(custom);
371 self
372 }
373
374 pub fn build(self) -> Notification<'a> {
375 self.notification
376 }
377}
378
379#[derive(Debug, Deserialize)]
380pub struct Response {
381 pub reason: ApiErrorReason,
382 pub timestamp: Option<i64>,
383 pub token: String,
384
385 pub apns_id: String,
386 #[serde(skip)]
387 pub(crate) status_code: StatusCode,
388}
389
390#[derive(Debug, Default)]
391pub struct BatchResponse {
392 pub success: i64,
393 pub failure: i64,
394 pub responses: Vec<Response>,
395}