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}