firebase_admin/messaging/message.rs
1//! Ergonomic, Rust-facing message types.
2//!
3//! These are distinct from the crate-internal wire-format DTOs used to talk
4//! to the FCM v1 REST API (see `crate::messaging::fcm_v1`), so that the
5//! public API never exposes Google's REST field naming or shapes directly.
6
7use std::collections::HashMap;
8
9/// A message to send via Firebase Cloud Messaging.
10///
11/// Exactly one of [`Self::target`] must be set to a token, topic, or
12/// condition — this is enforced by [`Target`] being a single field rather
13/// than three optional ones.
14#[derive(Debug, Clone)]
15pub struct Message {
16 /// Who the message is delivered to.
17 pub target: Target,
18 /// A simple, display notification shown by the client's OS.
19 pub notification: Option<Notification>,
20 /// Custom key-value payload delivered to the client app.
21 pub data: HashMap<String, String>,
22 /// Android-specific delivery options.
23 pub android: Option<AndroidConfig>,
24 /// Apple Push Notification Service-specific delivery options, as a raw
25 /// APNs payload (`aps` dictionary and custom keys).
26 pub apns: Option<ApnsConfig>,
27 /// Web Push-specific delivery options.
28 pub webpush: Option<WebpushConfig>,
29}
30
31impl Message {
32 /// Starts building a message addressed to a single device registration
33 /// token.
34 pub fn to_token(token: impl Into<String>) -> Self {
35 Self::new(Target::Token(token.into()))
36 }
37
38 /// Starts building a message addressed to a topic.
39 pub fn to_topic(topic: impl Into<String>) -> Self {
40 Self::new(Target::Topic(topic.into()))
41 }
42
43 /// Starts building a message addressed to devices matching a topic
44 /// condition expression, e.g. `"'A' in topics && 'B' in topics"`.
45 pub fn to_condition(condition: impl Into<String>) -> Self {
46 Self::new(Target::Condition(condition.into()))
47 }
48
49 fn new(target: Target) -> Self {
50 Self {
51 target,
52 notification: None,
53 data: HashMap::new(),
54 android: None,
55 apns: None,
56 webpush: None,
57 }
58 }
59
60 /// Sets a display notification.
61 pub fn with_notification(mut self, notification: Notification) -> Self {
62 self.notification = Some(notification);
63 self
64 }
65
66 /// Sets the custom data payload, replacing any previous value.
67 pub fn with_data(mut self, data: HashMap<String, String>) -> Self {
68 self.data = data;
69 self
70 }
71
72 /// Sets Android-specific delivery options.
73 pub fn with_android(mut self, android: AndroidConfig) -> Self {
74 self.android = Some(android);
75 self
76 }
77
78 /// Sets APNs-specific delivery options.
79 pub fn with_apns(mut self, apns: ApnsConfig) -> Self {
80 self.apns = Some(apns);
81 self
82 }
83
84 /// Sets Web Push-specific delivery options.
85 pub fn with_webpush(mut self, webpush: WebpushConfig) -> Self {
86 self.webpush = Some(webpush);
87 self
88 }
89}
90
91/// Who an FCM [`Message`] is delivered to.
92#[derive(Debug, Clone)]
93pub enum Target {
94 /// A single device registration token.
95 Token(String),
96 /// All devices subscribed to a topic.
97 Topic(String),
98 /// All devices matching a topic condition expression.
99 Condition(String),
100}
101
102/// A simple, display notification shown by the client's OS.
103#[derive(Debug, Default, Clone)]
104pub struct Notification {
105 /// The notification's title.
106 pub title: Option<String>,
107 /// The notification's body text.
108 pub body: Option<String>,
109 /// A URL to an image to display in the notification.
110 pub image: Option<String>,
111}
112
113/// Android-specific delivery options for a [`Message`].
114#[derive(Debug, Default, Clone)]
115pub struct AndroidConfig {
116 /// How long (in seconds) the message should be kept on FCM storage while
117 /// the device is offline.
118 pub ttl_seconds: Option<u64>,
119 /// An identifier used to collapse a group of messages, keeping only the
120 /// last one.
121 pub collapse_key: Option<String>,
122 /// Delivery priority: `"normal"` or `"high"`.
123 pub priority: Option<String>,
124}
125
126/// Apple Push Notification Service-specific delivery options for a
127/// [`Message`], as a raw APNs payload.
128#[derive(Debug, Default, Clone)]
129pub struct ApnsConfig {
130 /// Raw APNs headers, e.g. `apns-priority`, `apns-expiration`.
131 pub headers: HashMap<String, String>,
132 /// The raw APNs JSON payload, including the `aps` dictionary.
133 pub payload: Option<serde_json::Map<String, serde_json::Value>>,
134}
135
136/// Web Push-specific delivery options for a [`Message`].
137#[derive(Debug, Default, Clone)]
138pub struct WebpushConfig {
139 /// Raw Web Push protocol headers, e.g. `Urgency`, `TTL`.
140 pub headers: HashMap<String, String>,
141 /// Custom key-value payload delivered to the Web Push client, merged
142 /// with (and taking precedence over) [`Message::data`].
143 pub data: HashMap<String, String>,
144 /// A display notification shown by the browser, using the Web
145 /// Notifications API shape rather than [`Notification`]'s simplified
146 /// title/body/image.
147 pub notification: Option<WebpushNotification>,
148}
149
150/// A Web Push display notification, following the Web Notifications API
151/// (`https://developer.mozilla.org/docs/Web/API/Notification`) — the shape
152/// FCM forwards verbatim as `webpush.notification` to the browser's push
153/// event, distinct from [`Notification`]'s cross-platform title/body/image.
154#[derive(Debug, Default, Clone)]
155pub struct WebpushNotification {
156 /// The notification's title.
157 pub title: Option<String>,
158 /// The notification's body text.
159 pub body: Option<String>,
160 /// A URL to an icon to display in the notification.
161 pub icon: Option<String>,
162 /// Any additional Web Notification fields not covered above (e.g.
163 /// `badge`, `image`, `actions`, `vibrate`, `tag`), merged directly into
164 /// the wire-format `webpush.notification` object.
165 pub extra: serde_json::Map<String, serde_json::Value>,
166}
167
168/// The outcome of sending a single [`Message`] as part of a batch
169/// ([`crate::messaging::MessagingClient::send_each`] or
170/// [`crate::messaging::MessagingClient::send_each_for_multicast`]).
171#[derive(Debug, Clone)]
172pub enum SendResult {
173 /// The message was accepted; carries FCM's message id.
174 Success {
175 /// The FCM-assigned identifier for the sent message.
176 message_id: String,
177 },
178 /// The message was rejected.
179 Failure {
180 /// The error returned for this particular message.
181 error: SendError,
182 },
183}
184
185/// The error for a single failed message within a batch send.
186#[derive(Debug, Clone)]
187pub struct SendError {
188 /// HTTP status code returned for this message.
189 pub status: u16,
190 /// Human-readable error message.
191 pub message: String,
192 /// Machine-readable error code, when FCM provides one (e.g.
193 /// `UNREGISTERED`, `INVALID_ARGUMENT`, `QUOTA_EXCEEDED`).
194 pub error_code: Option<String>,
195}
196
197/// The response to [`crate::messaging::MessagingClient::send_each`] or
198/// [`crate::messaging::MessagingClient::send_each_for_multicast`].
199#[derive(Debug, Clone)]
200pub struct BatchResponse {
201 /// One result per input message, in the same order.
202 pub responses: Vec<SendResult>,
203 /// The number of messages that were successfully sent.
204 pub success_count: usize,
205 /// The number of messages that failed to send.
206 pub failure_count: usize,
207}
208
209impl BatchResponse {
210 pub(crate) fn from_results(responses: Vec<SendResult>) -> Self {
211 let success_count = responses
212 .iter()
213 .filter(|r| matches!(r, SendResult::Success { .. }))
214 .count();
215 let failure_count = responses.len() - success_count;
216 Self {
217 responses,
218 success_count,
219 failure_count,
220 }
221 }
222}
223
224/// The response to
225/// [`crate::messaging::MessagingClient::subscribe_to_topic`] or
226/// [`crate::messaging::MessagingClient::unsubscribe_from_topic`].
227///
228/// A single call can partially fail: some registration tokens may be
229/// invalid or unregistered while others succeed. Mirrors the official Admin
230/// SDKs' `TopicManagementResponse`, which reports per-token errors rather
231/// than failing the whole call when only some tokens are rejected.
232#[derive(Debug, Clone)]
233pub struct TopicManagementResponse {
234 /// The number of registration tokens that were successfully
235 /// subscribed/unsubscribed.
236 pub success_count: usize,
237 /// The number of registration tokens that failed.
238 pub failure_count: usize,
239 /// One entry per failed registration token, in the order they were
240 /// passed in.
241 pub errors: Vec<TopicManagementError>,
242}
243
244/// A single failed registration token within a [`TopicManagementResponse`].
245#[derive(Debug, Clone)]
246pub struct TopicManagementError {
247 /// The position of the failed token within the input slice.
248 pub index: usize,
249 /// The short error reason FCM returned for this token (e.g.
250 /// `NOT_FOUND`, `INVALID_ARGUMENT`).
251 pub reason: String,
252}
253
254impl TopicManagementResponse {
255 pub(crate) fn from_errors(total: usize, errors: Vec<TopicManagementError>) -> Self {
256 Self {
257 success_count: total - errors.len(),
258 failure_count: errors.len(),
259 errors,
260 }
261 }
262}