apple_apns/
request.rs

1use http::{header, HeaderMap, HeaderValue};
2use serde::Serialize;
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6use crate::header::*;
7use crate::payload::*;
8use crate::result::{Error, Result};
9
10/// Apple Push Notification service request options.
11#[derive(Clone, Debug, Default, PartialEq)]
12pub struct Request<T = ()> {
13    /// The hex-encoded device token.
14    pub device_token: String,
15
16    /// (Required for watchOS 6 and later; recommended for macOS, iOS, tvOS, and
17    /// iPadOS) The value of this header must accurately reflect the contents of
18    /// your notification’s payload. If there’s a mismatch, or if the header is
19    /// missing on required systems, APNs may return an error, delay the
20    /// delivery of the notification, or drop it altogether.
21    pub push_type: PushType,
22
23    /// A canonical UUID that is the unique ID for the notification. If an error
24    /// occurs when sending the notification, APNs includes this value when
25    /// reporting the error to your server. Canonical UUIDs are 32 lowercase
26    /// hexadecimal digits, displayed in five groups separated by hyphens in the
27    /// form 8-4-4-4-12. For example: 123e4567-e89b-12d3-a456-4266554400a0. If
28    /// you omit this header, APNs creates a UUID for you and returns it in its
29    /// response.
30    pub id: Option<Uuid>,
31
32    /// The date at which the notification is no longer valid. This value is a
33    /// UNIX epoch expressed in seconds (UTC). If the value is nonzero, APNs
34    /// stores the notification and tries to deliver it at least once, repeating
35    /// the attempt as needed until the specified date. If the value is 0, APNs
36    /// attempts to deliver the notification only once and doesn’t store it.
37    ///
38    /// A single APNs attempt may involve retries over multiple network
39    /// interfaces and connections of the destination device. Often these
40    /// retries span over some time period, depending on the network
41    /// characteristics. In addition, a push notification may take some time on
42    /// the network after APNs sends it to the device. APNs uses best efforts to
43    /// honor the expiry date without any guarantee. If the value is nonzero,
44    /// the notification may be delivered after the mentioned date. If the value
45    /// is 0, the notification may be delivered with some delay.
46    pub expiration: Option<OffsetDateTime>,
47
48    /// The priority of the notification. If you omit this header, APNs sets the
49    /// notification priority to 10.
50    ///
51    /// Specify 10 to send the notification immediately.
52    ///
53    /// Specify 5 to send the notification based on power considerations on the
54    /// user’s device.
55    ///
56    /// Specify 1 to prioritize the device’s power considerations over all other
57    /// factors for delivery, and prevent awakening the device.
58    pub priority: Priority,
59
60    /// The topic for the notification. In general, the topic is your app’s
61    /// bundle ID/app ID. It can have a suffix based on the type of push
62    /// notification. If you’re using a certificate that supports PushKit VoIP
63    /// or watchOS complication notifications, you must include this header with
64    /// bundle ID of you app and if applicable, the proper suffix. If you’re
65    /// using token-based authentication with APNs, you must include this header
66    /// with the correct bundle ID and suffix combination. To learn more about
67    /// app ID, see [Register an App
68    /// ID](https://help.apple.com/developer-account/#/dev1b35d6f83).
69    pub topic: Option<String>,
70
71    /// An identifier you use to coalesce multiple notifications into a single
72    /// notification for the user. Typically, each notification request causes a
73    /// new notification to be displayed on the user’s device. When sending the
74    /// same notification more than once, use the same value in this header to
75    /// coalesce the requests. The value of this key must not exceed 64 bytes.
76    pub collapse_id: Option<String>,
77
78    /// The information for displaying an alert.
79    pub alert: Option<Alert>,
80
81    /// The number to display in a badge on your app’s icon. Specify `0` to
82    /// remove the current badge, if any.
83    pub badge: Option<u32>,
84
85    /// The name of a sound file in your app’s main bundle or in the
86    /// `Library/Sounds` folder of your app’s container directory or a
87    /// dictionary that contains sound information for critical alerts.
88    pub sound: Option<Sound>,
89
90    /// An app-specific identifier for grouping related notifications. This
91    /// value corresponds to the
92    /// [`threadIdentifier`](https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier)
93    /// property in the `UNNotificationContent` object.
94    pub thread_id: Option<String>,
95
96    /// The notification’s type. This string must correspond to the
97    /// [`identifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcategory/1649276-identifier)
98    /// of one of the `UNNotificationCategory` objects you register at launch
99    /// time. See [Declaring Your Actionable Notification
100    /// Types](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types).
101    pub category: Option<String>,
102
103    /// The background notification flag. To perform a silent background update,
104    /// specify the value `1` and don’t include the `alert`, `badge`, or `sound`
105    /// keys in your payload. See [Pushing Background Updates to Your
106    /// App](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app).
107    pub content_available: bool,
108
109    /// The notification service app extension flag. If the value is `1`, the
110    /// system passes the notification to your notification service app
111    /// extension before delivery. Use your extension to modify the
112    /// notification’s content. See [Modifying Content in Newly Delivered
113    /// Notifications](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications).
114    pub mutable_content: bool,
115
116    /// The identifier of the window brought forward. The value of this key will
117    /// be populated on the
118    /// [`UNNotificationContent`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent)
119    /// object created from the push payload. Access the value using the
120    /// [`UNNotificationContent`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent)
121    /// object’s
122    /// [`targetContentIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/3235764-targetcontentidentifier)
123    /// property.
124    pub target_content_id: Option<String>,
125
126    /// The importance and delivery timing of a notification. The string values
127    /// `passive`, `active`, `time-sensitive`, or `critical` correspond to the
128    /// [`UNNotificationInterruptionLevel`](https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel)
129    /// enumeration cases.
130    pub interruption_level: Option<InterruptionLevel>,
131
132    /// The relevance score, a number between `0` and `1`, that the system uses
133    /// to sort the notifications from your app. The highest score gets featured
134    /// in the notification summary. See
135    /// [`relevanceScore`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/3821031-relevancescore).
136    pub relevance_score: Option<f64>,
137
138    /// Additional data to send.
139    pub user_info: Option<T>,
140}
141
142impl<T> TryFrom<Request<T>> for (HeaderMap<HeaderValue>, Payload<T>)
143where
144    T: Serialize,
145{
146    type Error = Error;
147
148    fn try_from(this: Request<T>) -> Result<Self> {
149        let mut headers = HeaderMap::new();
150
151        headers.insert(
152            header::CONTENT_TYPE,
153            HeaderValue::from_static("application/json"),
154        );
155
156        let _ = headers.insert(APNS_PUSH_TYPE.clone(), this.push_type.into());
157
158        if let Some(id) = this.id {
159            let id = id.hyphenated().to_string().parse()?;
160            let _ = headers.insert(APNS_ID.clone(), id);
161        }
162
163        if let Some(expiration) = this.expiration {
164            let expiration = expiration.unix_timestamp().to_string().parse()?;
165            let _ = headers.insert(APNS_EXPIRATION.clone(), expiration);
166        }
167
168        if this.priority != Priority::default() {
169            let _ = headers.insert(APNS_PRIORITY.clone(), this.priority.into());
170        }
171
172        if let Some(topic) = this.topic {
173            let topic = topic.parse()?;
174            let _ = headers.insert(APNS_TOPIC.clone(), topic);
175        }
176
177        if let Some(collapse_id) = this.collapse_id {
178            let collapse_id = collapse_id.parse()?;
179            let _ = headers.insert(APNS_COLLAPSE_ID.clone(), collapse_id);
180        }
181
182        let is_critical = this
183            .interruption_level
184            .as_ref()
185            .map(|il| *il == InterruptionLevel::Critical)
186            .unwrap_or_default();
187
188        let is_critical_sound = this
189            .sound
190            .as_ref()
191            .map(|sound| sound.critical)
192            .unwrap_or_default();
193
194        if is_critical != is_critical_sound {
195            return Err(Error::CriticalSound);
196        }
197
198        let sound = this.sound.map(|mut sound| {
199            sound.critical = is_critical || is_critical_sound;
200            sound
201        });
202
203        let payload = Payload {
204            aps: Aps {
205                alert: this.alert.map(Into::into),
206                badge: this.badge,
207                sound,
208                thread_id: this.thread_id,
209                category: this.category,
210                content_available: this.content_available,
211                mutable_content: this.mutable_content,
212                target_content_id: this.target_content_id,
213                interruption_level: this.interruption_level,
214                relevance_score: this.relevance_score,
215            },
216            user_info: this.user_info,
217        };
218
219        Ok((headers, payload))
220    }
221}