1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
pub use chrono::{DateTime, FixedOffset, Duration};

/// A description of what went wrong with the push notification.
/// Referred from [Firebase documentation](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9)
#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
pub enum ErrorReason {
    /// Check that the request contains a registration token (in the `to` or
    /// `registration_ids` field).
    MissingRegistration,

    /// Check the format of the registration token you pass to the server. Make
    /// sure it matches the registration token the client app receives from
    /// registering with Firebase Notifications. Do not truncate or add
    /// additional characters.
    InvalidRegistration,

    /// An existing registration token may cease to be valid in a number of
    /// scenarios, including:
    ///
    /// * If the client app unregisters with FCM.
    /// * If the client app is automatically unregistered, which can happen if
    ///   the user uninstalls the application. For example, on iOS, if the APNS
    ///   Feedback Service reported the APNS token as invalid.
    /// * If the registration token expires (for example, Google might decide to
    ///   refresh registration tokens, or the APNS token has expired for iOS
    ///   devices).
    /// * If the client app is updated but the new version is not configured to
    ///   receive messages.
    ///
    /// For all these cases, remove this registration token from the app server
    /// and stop using it to send messages.
    NotRegistered,

    /// Make sure the message was addressed to a registration token whose
    /// package name matches the value passed in the request.
    InvalidPackageName,

    /// A registration token is tied to a certain group of senders. When a
    /// client app registers for FCM, it must specify which senders are allowed
    /// to send messages. You should use one of those sender IDs when sending
    /// messages to the client app. If you switch to a different sender, the
    /// existing registration tokens won't work.
    MismatchSenderId,

    /// Check that the provided parameters have the right name and type.
    InvalidParameters,

    /// Check that the total size of the payload data included in a message does
    /// not exceed FCM limits: 4096 bytes for most messages, or 2048 bytes in
    /// the case of messages to topics. This includes both the keys and the
    /// values.
    MessageTooBig,

    /// Check that the custom payload data does not contain a key (such as
    /// `from`, or `gcm`, or any value prefixed by google) that is used
    /// internally by FCM. Note that some words (such as `collapse_key`) are
    /// also used by FCM but are allowed in the payload, in which case the
    /// payload value will be overridden by the FCM value.
    InvalidDataKey,

    /// Check that the value used in `time_to_live` is an integer representing a
    /// duration in seconds between 0 and 2,419,200 (4 weeks).
    InvalidTtl,

    /// In internal use only. Check
    /// [FcmError::ServerError](enum.FcmError.html#variant.ServerError).
    Unavailable,

    /// In internal use only. Check
    /// [FcmError::ServerError](enum.FcmError.html#variant.ServerError).
    InternalServerError,

    /// The rate of messages to a particular device is too high. If an iOS app
    /// sends messages at a rate exceeding APNs limits, it may receive this
    /// error message
    ///
    /// Reduce the number of messages sent to this device and use exponential
    /// backoff to retry sending.
    DeviceMessageRateExceeded,

    /// The rate of messages to subscribers to a particular topic is too high.
    /// Reduce the number of messages sent for this topic and use exponential
    /// backoff to retry sending.
    TopicsMessageRateExceeded,

    /// A message targeted to an iOS device could not be sent because the
    /// required APNs authentication key was not uploaded or has expired. Check
    /// the validity of your development and production credentials.
    InvalidApnsCredential,
}

#[derive(Deserialize, Debug)]
pub struct FcmResponse {
    pub message_id: Option<u64>,
    pub error: Option<ErrorReason>,
    pub multicast_id: Option<i64>,
    pub success: Option<u64>,
    pub failure: Option<u64>,
    pub canonical_ids: Option<u64>,
    pub results: Option<Vec<MessageResult>>,
}

#[derive(Deserialize, Debug)]
pub struct MessageResult {
    pub message_id: Option<String>,
    pub registration_id: Option<String>,
    pub error: Option<ErrorReason>,
}

/// Fatal errors. Referred from [Firebase
/// documentation](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9)
#[derive(PartialEq, Debug)]
pub enum FcmError {
    /// The sender account used to send a message couldn't be authenticated. Possible causes are:
    ///
    /// Authorization header missing or with invalid syntax in HTTP request.
    ///
    /// * The Firebase project that the specified server key belongs to is
    ///   incorrect.
    /// * Legacy server keys only—the request originated from a server not
    ///   whitelisted in the Server key IPs.
    ///
    /// Check that the token you're sending inside the Authentication header is
    /// the correct Server key associated with your project. See Checking the
    /// validity of a Server key for details. If you are using a legacy server
    /// key, you're recommended to upgrade to a new key that has no IP
    /// restrictions.
    Unauthorized,

    /// Check that the JSON message is properly formatted and contains valid
    /// fields (for instance, making sure the right data type is passed in).
    InvalidMessage(String),

    /// The server couldn't process the request. Retry the same request, but you must:
    ///
    /// * Honor the [RetryAfter](enum.RetryAfter.html) value if included.
    /// * Implement exponential back-off in your retry mechanism. (e.g. if you
    ///   waited one second before the first retry, wait at least two second
    ///   before the next one, then 4 seconds and so on). If you're sending
    ///   multiple messages, delay each one independently by an additional random
    ///   amount to avoid issuing a new request for all messages at the same time.
    ///
    /// Senders that cause problems risk being blacklisted.
    ServerError(Option<RetryAfter>),
}

#[derive(PartialEq, Debug)]
pub enum RetryAfter {
    /// Amount of time to wait until retrying the message is allowed.
    Delay(Duration),

    /// A point in time until retrying the message is allowed.
    DateTime(DateTime<FixedOffset>),
}

impl RetryAfter {
    pub fn from_str(header_value: &str) -> Option<RetryAfter> {
        if let Ok(seconds) = header_value.parse::<i64>() {
            Some(RetryAfter::Delay(Duration::seconds(seconds)))
        } else {
            DateTime::parse_from_rfc2822(header_value)
                .map(|date_time| {
                    RetryAfter::DateTime(date_time)
                })
                .ok()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{DateTime, Duration};
    use serde_json;

    #[test]
    fn test_some_errors() {
        let errors = vec![
            ("MissingRegistration", ErrorReason::MissingRegistration),
            ("InvalidRegistration", ErrorReason::InvalidRegistration),
            ("NotRegistered", ErrorReason::NotRegistered),
            ("InvalidPackageName", ErrorReason::InvalidPackageName),
            ("MismatchSenderId", ErrorReason::MismatchSenderId),
            ("InvalidParameters", ErrorReason::InvalidParameters),
            ("MessageTooBig", ErrorReason::MessageTooBig),
            ("InvalidDataKey", ErrorReason::InvalidDataKey),
            ("InvalidTtl", ErrorReason::InvalidTtl),
            ("Unavailable", ErrorReason::Unavailable),
            ("InternalServerError", ErrorReason::InternalServerError),
            ("DeviceMessageRateExceeded", ErrorReason::DeviceMessageRateExceeded),
            ("TopicsMessageRateExceeded", ErrorReason::TopicsMessageRateExceeded),
            ("InvalidApnsCredential", ErrorReason::InvalidApnsCredential),
        ];

        for (error_str, error_enum) in errors.into_iter() {
            let response_data = json!({
                "error": error_str,
                "results": [
                    {"error": error_str}
                ]
            });

            let response_string = serde_json::to_string(&response_data).unwrap();
            let fcm_response: FcmResponse = serde_json::from_str(&response_string).unwrap();

            assert_eq!(
                Some(error_enum.clone()),
                fcm_response.results.unwrap()[0].error,
            );

            assert_eq!(
                Some(error_enum),
                fcm_response.error,
            )
        }
    }

    #[test]
    fn test_retry_after_from_seconds() {
        assert_eq!(
            Some(RetryAfter::Delay(Duration::seconds(420))),
            RetryAfter::from_str("420")
        );
    }

    #[test]
    fn test_retry_after_from_date() {
        let date = "Sun, 06 Nov 1994 08:49:37 GMT";
        let retry_after = RetryAfter::from_str(date);

        assert_eq!(
            Some(RetryAfter::DateTime(
                DateTime::parse_from_rfc2822(date).unwrap()
            )),
            retry_after,
        );
    }
}