Skip to main content

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}