hi_push/apns/
types.rs

1use std::collections::HashMap;
2
3use reqwest::StatusCode;
4use serde::{Deserialize, Serialize};
5use serde_repr::{Deserialize_repr, Serialize_repr};
6use uuid::Uuid;
7
8/// The reason for a failure returned by the APN api.
9#[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
80/// APNS production endpoint.
81pub static APN_URL_PRODUCTION: &'static str = "https://api.push.apple.com";
82
83/// APNS development endpoint.
84pub static APN_URL_DEV: &'static str = "https://api.sandbox.push.apple.com";
85
86/// Notification priority.
87/// See APNS documentation for the effects.
88#[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy, Debug)]
89#[repr(u8)]
90pub enum Priority {
91    Low = 1,
92    // 5
93    Middle = 5,
94    High = 10, // 10
95}
96
97impl Priority {
98    /// Convert Priority to it's numeric value.
99    pub fn to_uint(self) -> u8 {
100        self as u8
101    }
102}
103
104#[derive(Debug)]
105pub struct CollapseIdTooLongError;
106
107/// Wrapper type for collapse ids.
108/// It may be an arbitrary string, but is limited in length to at most 63 bytes.
109#[derive(Serialize, Clone, Debug)]
110pub struct CollapseId<'a>(&'a str);
111
112impl<'a> CollapseId<'a> {
113    /// Construct a new collapse id.
114    /// Returns an error if id exceeds the maximum length of 64 bytes.
115    pub fn new(value: &'a str) -> Result<Self, CollapseIdTooLongError> {
116        // CollapseID must be at most 64 bytes long.
117        if value.len() > 64 {
118            Err(CollapseIdTooLongError)
119        } else {
120            Ok(CollapseId(value))
121        }
122    }
123
124    /// Get id as a raw str.
125    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/// Alert content for a notification.
157///
158/// See the official documentation for details:
159/// https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html
160#[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/// The alert content.
196/// This can either be a plain message string, or an AlertPayload with more
197/// configuration.
198#[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    /// Updates the numeric badge for the app. Set to 0 to remove.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub badge: Option<u32>,
212    /// Sound to play. Use 'default' for the default sound.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub sound: Option<&'a str>,
215    /// Set to true to mark the app as having content available.
216    #[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/// A full json request object for sending a notification to the API.
225#[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/// A notification struct contains all relevant data for a notification request
233/// sent to the APNS API.
234/// This includes other options not contained in the payload.
235/// These options are transferred with HTTP request headers.
236#[derive(Serialize, Clone, Debug)]
237pub struct Notification<'a> {
238    /// The topic to use. Usually the app bundle id.
239    pub topic: &'a str,
240    pub device_token: &'a str,
241    pub payload: Payload<'a>,
242
243    /// Optional id identifying the message.
244    pub id: Option<Uuid>,
245    /// Optional expiration time as UNIX timestamp.
246    pub expiration: Option<u64>,
247    /// Priority for the notification.
248    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    /// Create a new notification.
256    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
271/// A builder for convenient construction of notifications.
272pub 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}