diff --git a/README.md b/README.md index b74bfbd09..7c361866b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,70 @@ This fork is a rewrite to use Google's HTTP v1 API. + +# Getting started + +## Installation + +Add the following to your `Cargo.toml` file: + +```toml +[dependencies] +fcm = { git = "https://github.com/rj76/fcm-rust.git" } +``` + +Then, you need to add the credentials described in the [Credentials](#credentials) to a `.env` file at the root of your project. + +## Usage + +For a complete usage example, you may check the [Examples](#examples) section. + +### Import + +```rust +use fcm; +``` + +### Create a client instance + +```rust +let client = fcm::Client::new(); +``` + +### Construct a message + +```rust +let message = fcm::Message { + data: None, + notification: Some(Notification { + title: Some("I'm high".to_string()), + body: Some(format!("it's {}", chrono::Utc::now())), + ..Default::default() + }), + target: Target::Token(device_token), + fcm_options: Some(FcmOptions { + analytics_label: "analytics_label".to_string(), + }), + android: Some(AndroidConfig { + priority: Some(fcm::AndroidMessagePriority::High), + notification: Some(AndroidNotification { + title: Some("I'm Android high".to_string()), + body: Some(format!("Hi Android, it's {}", chrono::Utc::now())), + ..Default::default() + }), + ..Default::default() + }), + apns: Some(ApnsConfig { ..Default::default() }), + webpush: Some(WebpushConfig { ..Default::default() }), +} +``` + +### Send the message + +```rust +let response = client.send(message).await?; +``` + # Credentials This library expects the Google credentials JSON location to be @@ -19,4 +83,13 @@ Please follow the instructions in the [Firebase Documentation](https://firebase. ## Examples -Check out the examples directory for a simple sender. +For a complete usage example, you may check out the [`simple_sender`](examples/simple_sender.rs) example. + +To run the example, first of all clone the [`.env.example`](.env.example) file to `.env` and fill in the required values. + +You can find info about the required credentials in the [Credentials](#credentials) section. + +Then run the example with `cargo run --example simple_sender -- -t ` + + + diff --git a/examples/simple_sender.rs b/examples/simple_sender.rs index 6a634b801..e5fc85300 100644 --- a/examples/simple_sender.rs +++ b/examples/simple_sender.rs @@ -1,11 +1,10 @@ -use argparse::{ArgumentParser, Store}; -use fcm::{Client, MessageBuilder, Target}; -use serde::Serialize; +// cargo run --example simple_sender -- -t -#[derive(Serialize)] -struct CustomData { - message: &'static str, -} +use argparse::{ArgumentParser, Store}; +use fcm::{ + AndroidConfig, AndroidNotification, ApnsConfig, Client, FcmOptions, Message, Notification, Target, WebpushConfig, +}; +use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { @@ -22,12 +21,36 @@ async fn main() -> Result<(), Box> { } let client = Client::new(); - let data = CustomData { message: "howdy" }; - - let mut builder = MessageBuilder::new(Target::Token(device_token)); - builder.data(&data)?; - let response = client.send(builder.finalize()).await?; + let data = json!({ + "key": "value", + }); + + let builder = Message { + data: Some(data), + notification: Some(Notification { + title: Some("I'm high".to_string()), + body: Some(format!("it's {}", chrono::Utc::now())), + ..Default::default() + }), + target: Target::Token(device_token), + fcm_options: Some(FcmOptions { + analytics_label: "analytics_label".to_string(), + }), + android: Some(AndroidConfig { + priority: Some(fcm::AndroidMessagePriority::High), + notification: Some(AndroidNotification { + title: Some("I'm Android high".to_string()), + body: Some(format!("Hi Android, it's {}", chrono::Utc::now())), + ..Default::default() + }), + ..Default::default() + }), + apns: Some(ApnsConfig { ..Default::default() }), + webpush: Some(WebpushConfig { ..Default::default() }), + }; + + let response = client.send(builder).await?; println!("Sent: {:?}", response); Ok(()) diff --git a/src/android/android_config.rs b/src/android/android_config.rs new file mode 100644 index 000000000..c6ef78756 --- /dev/null +++ b/src/android/android_config.rs @@ -0,0 +1,80 @@ +use serde::Serialize; +use serde_json::Value; + +use super::{ + android_fcm_options::{AndroidFcmOptions, AndroidFcmOptionsInternal}, + android_message_priority::AndroidMessagePriority, + android_notification::{AndroidNotification, AndroidNotificationInternal}, +}; + +#[derive(Serialize, Debug)] +pub(crate) struct AndroidConfigInternal { + #[serde(skip_serializing_if = "Option::is_none")] + collapse_key: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + priority: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + ttl: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + restricted_package_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + notification: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + fcm_options: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + direct_boot_ok: Option, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidconfig +pub struct AndroidConfig { + /// An identifier of a group of messages that can be collapsed, so that only the last message gets + /// sent when delivery can be resumed. + pub collapse_key: Option, + + /// Message priority. + pub priority: Option, + + /// How long (in seconds) the message should be kept in FCM storage if the device is offline. + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + pub ttl: Option, + + /// Package name of the application where the registration token must match in order to receive the message. + pub restricted_package_name: Option, + + /// Arbitrary key/value payload. + pub data: Option, + + /// Notification to send to android devices. + pub notification: Option, + + /// Options for features provided by the FCM SDK for Android. + pub fcm_options: Option, + + /// If set to true, messages will be allowed to be delivered to the app while the device is in direct boot mode. + pub direct_boot_ok: Option, +} + +impl AndroidConfig { + pub(crate) fn finalize(self) -> AndroidConfigInternal { + AndroidConfigInternal { + collapse_key: self.collapse_key, + priority: self.priority, + ttl: self.ttl, + restricted_package_name: self.restricted_package_name, + data: self.data, + notification: self.notification.map(|n| n.finalize()), + fcm_options: self.fcm_options.map(|f| f.finalize()), + direct_boot_ok: self.direct_boot_ok, + } + } +} diff --git a/src/android/android_fcm_options.rs b/src/android/android_fcm_options.rs new file mode 100644 index 000000000..212d6a0cc --- /dev/null +++ b/src/android/android_fcm_options.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +pub(crate) struct AndroidFcmOptionsInternal { + analytics_label: String, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidconfig +pub struct AndroidFcmOptions { + /// Label associated with the message's analytics data. + pub analytics_label: String, +} + +impl AndroidFcmOptions { + pub(crate) fn finalize(self) -> AndroidFcmOptionsInternal { + AndroidFcmOptionsInternal { + analytics_label: self.analytics_label, + } + } +} diff --git a/src/android/android_message_priority.rs b/src/android/android_message_priority.rs new file mode 100644 index 000000000..aa26c9c7c --- /dev/null +++ b/src/android/android_message_priority.rs @@ -0,0 +1,10 @@ +use serde::Serialize; + +#[allow(dead_code)] +#[derive(Serialize, Debug)] +#[serde(rename_all = "UPPERCASE")] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidmessagepriority +pub enum AndroidMessagePriority { + Normal, + High, +} diff --git a/src/android/android_notification.rs b/src/android/android_notification.rs new file mode 100644 index 000000000..eba41511a --- /dev/null +++ b/src/android/android_notification.rs @@ -0,0 +1,234 @@ +use serde::Serialize; + +use super::{ + light_settings::{LightSettings, LightSettingsInternal}, + notification_priority::NotificationPriority, + visibility::Visibility, +}; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidnotification +pub(crate) struct AndroidNotificationInternal { + /// The notification's title. + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + + /// The notification's body text. + #[serde(skip_serializing_if = "Option::is_none")] + body: Option, + + /// The notification's icon. + #[serde(skip_serializing_if = "Option::is_none")] + icon: Option, + + /// The notification's icon color, expressed in #rrggbb format. + #[serde(skip_serializing_if = "Option::is_none")] + color: Option, + + /// The sound to play when the device receives the notification. + #[serde(skip_serializing_if = "Option::is_none")] + sound: Option, + + /// Identifier used to replace existing notifications in the notification drawer. + #[serde(skip_serializing_if = "Option::is_none")] + tag: Option, + + /// The action associated with a user click on the notification. + #[serde(skip_serializing_if = "Option::is_none")] + click_action: Option, + + /// The key to the body string in the app's string resources to use to localize the body text to the user's + /// current localization. + #[serde(skip_serializing_if = "Option::is_none")] + body_loc_key: Option, + + /// Variable string values to be used in place of the format specifiers in body_loc_key to use to localize the + /// body text to the user's current localization. + #[serde(skip_serializing_if = "Option::is_none")] + body_loc_args: Option>, + + /// The key to the title string in the app's string resources to use to localize the title text to the user's + /// current localization. + #[serde(skip_serializing_if = "Option::is_none")] + title_loc_key: Option, + + /// Variable string values to be used in place of the format specifiers in title_loc_key to use to localize the + /// title text to the user's current localization. + #[serde(skip_serializing_if = "Option::is_none")] + title_loc_args: Option>, + + /// The notification's channel id (new in Android O). + #[serde(skip_serializing_if = "Option::is_none")] + channel_id: Option, + + /// Sets the "ticker" text, which is sent to accessibility services. + #[serde(skip_serializing_if = "Option::is_none")] + ticker: Option, + + /// When set to false or unset, the notification is automatically dismissed when the user clicks it in the panel. + #[serde(skip_serializing_if = "Option::is_none")] + sticky: Option, + + /// Set the time that the event in the notification occurred. Notifications in the panel are sorted by this time. + /// Timestamp format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Timestamp + #[serde(skip_serializing_if = "Option::is_none")] + event_time: Option, + + /// Set whether or not this notification is relevant only to the current device. + #[serde(skip_serializing_if = "Option::is_none")] + local_only: Option, + + /// Set the relative priority for this notification. + #[serde(skip_serializing_if = "Option::is_none")] + notification_priority: Option, + + /// If set to true, use the Android framework's default sound for the notification. + #[serde(skip_serializing_if = "Option::is_none")] + default_sound: Option, + + /// If set to true, use the Android framework's default vibrate pattern for the notification. + #[serde(skip_serializing_if = "Option::is_none")] + default_vibrate_timings: Option, + + /// If set to true, use the Android framework's default LED light settings for the notification. + #[serde(skip_serializing_if = "Option::is_none")] + default_light_settings: Option, + + /// Set the vibration pattern to use + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + #[serde(skip_serializing_if = "Option::is_none")] + vibrate_timings: Option>, + + /// Set the Notification.visibility of the notification. + #[serde(skip_serializing_if = "Option::is_none")] + visibility: Option, + + /// Sets the number of items this notification represents. + #[serde(skip_serializing_if = "Option::is_none")] + notification_count: Option, + + /// Settings to control the notification's LED blinking rate and color if LED is available on the device. + #[serde(skip_serializing_if = "Option::is_none")] + light_settings: Option, + + /// Contains the URL of an image that is going to be displayed in a notification. + #[serde(skip_serializing_if = "Option::is_none")] + image: Option, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidnotification +pub struct AndroidNotification { + /// The notification's title. + pub title: Option, + + /// The notification's body text. + pub body: Option, + + /// The notification's icon. + pub icon: Option, + + /// The notification's icon color, expressed in #rrggbb format. + pub color: Option, + + /// The sound to play when the device receives the notification. + pub sound: Option, + + /// Identifier used to replace existing notifications in the notification drawer. + pub tag: Option, + + /// The action associated with a user click on the notification. + pub click_action: Option, + + /// The key to the body string in the app's string resources to use to localize the body text to the user's + /// current localization. + pub body_loc_key: Option, + + /// Variable string values to be used in place of the format specifiers in body_loc_key to use to localize the + /// body text to the user's current localization. + pub body_loc_args: Option>, + + /// The key to the title string in the app's string resources to use to localize the title text to the user's + /// current localization. + pub title_loc_key: Option, + + /// Variable string values to be used in place of the format specifiers in title_loc_key to use to localize the + /// title text to the user's current localization. + pub title_loc_args: Option>, + + /// The notification's channel id (new in Android O). + pub channel_id: Option, + + /// Sets the "ticker" text, which is sent to accessibility services. + pub ticker: Option, + + /// When set to false or unset, the notification is automatically dismissed when the user clicks it in the panel. + pub sticky: Option, + + /// Set the time that the event in the notification occurred. Notifications in the panel are sorted by this time. + /// Timestamp format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Timestamp + pub event_time: Option, + + /// Set whether or not this notification is relevant only to the current device. + pub local_only: Option, + + /// Set the relative priority for this notification. + pub notification_priority: Option, + + /// If set to true, use the Android framework's default sound for the notification. + pub default_sound: Option, + + /// If set to true, use the Android framework's default vibrate pattern for the notification. + pub default_vibrate_timings: Option, + + /// If set to true, use the Android framework's default LED light settings for the notification. + pub default_light_settings: Option, + + /// Set the vibration pattern to use + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + pub vibrate_timings: Option>, + + /// Set the Notification.visibility of the notification. + pub visibility: Option, + + /// Sets the number of items this notification represents. + pub notification_count: Option, + + /// Settings to control the notification's LED blinking rate and color if LED is available on the device. + pub light_settings: Option, + + /// Contains the URL of an image that is going to be displayed in a notification. + pub image: Option, +} + +impl AndroidNotification { + pub(crate) fn finalize(self) -> AndroidNotificationInternal { + AndroidNotificationInternal { + title: self.title, + body: self.body, + icon: self.icon, + color: self.color, + sound: self.sound, + tag: self.tag, + click_action: self.click_action, + body_loc_key: self.body_loc_key, + body_loc_args: self.body_loc_args, + title_loc_key: self.title_loc_key, + title_loc_args: self.title_loc_args, + channel_id: self.channel_id, + ticker: self.ticker, + sticky: self.sticky, + event_time: self.event_time, + local_only: self.local_only, + notification_priority: self.notification_priority, + default_sound: self.default_sound, + default_vibrate_timings: self.default_vibrate_timings, + default_light_settings: self.default_light_settings, + vibrate_timings: self.vibrate_timings, + visibility: self.visibility, + notification_count: self.notification_count, + light_settings: self.light_settings.map(|x| x.finalize()), + image: self.image, + } + } +} diff --git a/src/android/color.rs b/src/android/color.rs new file mode 100644 index 000000000..a22c0077c --- /dev/null +++ b/src/android/color.rs @@ -0,0 +1,44 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#Color +pub(crate) struct ColorInternal { + /// The amount of red in the color as a value in the interval [0, 1]. + red: f32, + + /// The amount of green in the color as a value in the interval [0, 1]. + green: f32, + + /// The amount of blue in the color as a value in the interval [0, 1]. + blue: f32, + + /// The fraction of this color that should be applied to the pixel. + alpha: f32, +} + +#[derive(Debug, Default, Serialize)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#Color +pub struct Color { + /// The amount of red in the color as a value in the interval [0, 1]. + pub red: f32, + + /// The amount of green in the color as a value in the interval [0, 1]. + pub green: f32, + + /// The amount of blue in the color as a value in the interval [0, 1]. + pub blue: f32, + + /// The fraction of this color that should be applied to the pixel. + pub alpha: f32, +} + +impl Color { + pub(crate) fn finalize(self) -> ColorInternal { + ColorInternal { + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha, + } + } +} diff --git a/src/android/light_settings.rs b/src/android/light_settings.rs new file mode 100644 index 000000000..1a8850932 --- /dev/null +++ b/src/android/light_settings.rs @@ -0,0 +1,43 @@ +use serde::Serialize; + +use super::color::{Color, ColorInternal}; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#LightSettings +pub(crate) struct LightSettingsInternal { + /// Set color of the LED with google.type.Color. + color: ColorInternal, + + /// Along with light_off_duration, define the blink rate of LED flashes + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + light_on_duration: String, + + /// Along with light_on_duration, define the blink rate of LED flashes. + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + light_off_duration: String, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#LightSettings +pub struct LightSettings { + /// Set color of the LED with google.type.Color. + pub color: Color, + + /// Along with light_off_duration, define the blink rate of LED flashes + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + pub light_on_duration: String, + + /// Along with light_on_duration, define the blink rate of LED flashes. + /// Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration + pub light_off_duration: String, +} + +impl LightSettings { + pub(crate) fn finalize(self) -> LightSettingsInternal { + LightSettingsInternal { + color: self.color.finalize(), + light_on_duration: self.light_on_duration, + light_off_duration: self.light_off_duration, + } + } +} diff --git a/src/android/mod.rs b/src/android/mod.rs index a0f1778a0..b4ba2e81b 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -1,221 +1,8 @@ -use serde::Serialize; -use serde_json::Value; - -#[derive(Serialize, Debug)] -//https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidconfig -pub struct AndroidConfig { - // An identifier of a group of messages that can be collapsed, so that only the last message gets - // sent when delivery can be resumed. - #[serde(skip_serializing_if = "Option::is_none")] - collapse_key: Option, - - // Message priority. - #[serde(skip_serializing_if = "Option::is_none")] - priority: Option, - - // How long (in seconds) the message should be kept in FCM storage if the device is offline. - // Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration - #[serde(skip_serializing_if = "Option::is_none")] - ttl: Option, - - // Package name of the application where the registration token must match in order to receive the message. - #[serde(skip_serializing_if = "Option::is_none")] - restricted_package_name: Option, - - // Arbitrary key/value payload. - #[serde(skip_serializing_if = "Option::is_none")] - data: Option, - - // Notification to send to android devices. - #[serde(skip_serializing_if = "Option::is_none")] - notification: Option, - - // Options for features provided by the FCM SDK for Android. - #[serde(skip_serializing_if = "Option::is_none")] - fcm_options: Option, - - // If set to true, messages will be allowed to be delivered to the app while the device is in direct boot mode. - #[serde(skip_serializing_if = "Option::is_none")] - direct_boot_ok: Option, -} - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#Color -pub struct Color { - // The amount of red in the color as a value in the interval [0, 1]. - red: f32, - - // The amount of green in the color as a value in the interval [0, 1]. - green: f32, - - // The amount of blue in the color as a value in the interval [0, 1]. - blue: f32, - - // The fraction of this color that should be applied to the pixel. - alpha: f32, -} - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#LightSettings -pub struct LightSettings { - // Set color of the LED with google.type.Color. - color: Color, - - // Along with light_off_duration, define the blink rate of LED flashes - // Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration - light_on_duration: String, - - // Along with light_on_duration, define the blink rate of LED flashes. - // Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration - light_off_duration: String, -} - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidnotification -pub struct AndroidNotification { - // The notification's title. - #[serde(skip_serializing_if = "Option::is_none")] - title: Option, - - // The notification's body text. - #[serde(skip_serializing_if = "Option::is_none")] - body: Option, - - // The notification's icon. - #[serde(skip_serializing_if = "Option::is_none")] - icon: Option, - - // The notification's icon color, expressed in #rrggbb format. - #[serde(skip_serializing_if = "Option::is_none")] - color: Option, - - // The sound to play when the device receives the notification. - #[serde(skip_serializing_if = "Option::is_none")] - sound: Option, - - // Identifier used to replace existing notifications in the notification drawer. - #[serde(skip_serializing_if = "Option::is_none")] - tag: Option, - - // The action associated with a user click on the notification. - #[serde(skip_serializing_if = "Option::is_none")] - click_action: Option, - - // The key to the body string in the app's string resources to use to localize the body text to the user's - // current localization. - #[serde(skip_serializing_if = "Option::is_none")] - body_loc_key: Option, - - // Variable string values to be used in place of the format specifiers in body_loc_key to use to localize the - // body text to the user's current localization. - #[serde(skip_serializing_if = "Option::is_none")] - body_loc_args: Option>, - - // The key to the title string in the app's string resources to use to localize the title text to the user's - // current localization. - #[serde(skip_serializing_if = "Option::is_none")] - title_loc_key: Option, - - // Variable string values to be used in place of the format specifiers in title_loc_key to use to localize the - // title text to the user's current localization. - #[serde(skip_serializing_if = "Option::is_none")] - title_loc_args: Option>, - - // The notification's channel id (new in Android O). - #[serde(skip_serializing_if = "Option::is_none")] - channel_id: Option, - - // Sets the "ticker" text, which is sent to accessibility services. - #[serde(skip_serializing_if = "Option::is_none")] - ticker: Option, - - // When set to false or unset, the notification is automatically dismissed when the user clicks it in the panel. - #[serde(skip_serializing_if = "Option::is_none")] - sticky: Option, - - // Set the time that the event in the notification occurred. Notifications in the panel are sorted by this time. - // Timestamp format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Timestamp - #[serde(skip_serializing_if = "Option::is_none")] - event_time: Option, - - // Set whether or not this notification is relevant only to the current device. - #[serde(skip_serializing_if = "Option::is_none")] - local_only: Option, - - // Set the relative priority for this notification. - #[serde(skip_serializing_if = "Option::is_none")] - notification_priority: Option, - - // If set to true, use the Android framework's default sound for the notification. - #[serde(skip_serializing_if = "Option::is_none")] - default_sound: Option, - - // If set to true, use the Android framework's default vibrate pattern for the notification. - #[serde(skip_serializing_if = "Option::is_none")] - default_vibrate_timings: Option, - - // If set to true, use the Android framework's default LED light settings for the notification. - #[serde(skip_serializing_if = "Option::is_none")] - default_light_settings: Option, - - // Set the vibration pattern to use - // Duration format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Duration - #[serde(skip_serializing_if = "Option::is_none")] - vibrate_timings: Option>, - - // Set the Notification.visibility of the notification. - #[serde(skip_serializing_if = "Option::is_none")] - visibility: Option, - - // Sets the number of items this notification represents. - #[serde(skip_serializing_if = "Option::is_none")] - notification_count: Option, - - // Settings to control the notification's LED blinking rate and color if LED is available on the device. - #[serde(skip_serializing_if = "Option::is_none")] - light_settings: Option, - - // Contains the URL of an image that is going to be displayed in a notification. - #[serde(skip_serializing_if = "Option::is_none")] - image: Option, -} - -#[derive(Serialize, Debug)] -//https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidconfig -pub struct AndroidFcmOptions { - // Label associated with the message's analytics data. - analytics_label: String, -} - -#[allow(dead_code)] -#[derive(Serialize, Debug)] -#[serde(rename_all = "UPPERCASE")] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#androidmessagepriority -pub enum AndroidMessagePriority { - Normal, - High, -} - -#[allow(dead_code)] -#[derive(Serialize, Debug)] -#[serde(rename_all = "UPPERCASE")] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#notificationpriority -pub enum NotificationPriority { - PriorityUnspecified, - PriorityMin, - PriorityLow, - PriorityDefault, - PriorityHigh, - PriorityMax, -} - -#[allow(dead_code)] -#[derive(Serialize, Debug)] -#[serde(rename_all = "UPPERCASE")] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#visibility -pub enum Visibility { - VisibilityUnspecified, - Private, - Public, - Secret, -} +pub mod android_config; +pub mod android_fcm_options; +pub mod android_message_priority; +pub mod android_notification; +pub mod color; +pub mod light_settings; +pub mod notification_priority; +pub mod visibility; diff --git a/src/android/notification_priority.rs b/src/android/notification_priority.rs new file mode 100644 index 000000000..1d21c6e03 --- /dev/null +++ b/src/android/notification_priority.rs @@ -0,0 +1,14 @@ +use serde::Serialize; + +#[allow(dead_code)] +#[derive(Serialize, Debug)] +#[serde(rename_all = "UPPERCASE")] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#notificationpriority +pub enum NotificationPriority { + PriorityUnspecified, + PriorityMin, + PriorityLow, + PriorityDefault, + PriorityHigh, + PriorityMax, +} diff --git a/src/android/visibility.rs b/src/android/visibility.rs new file mode 100644 index 000000000..d07d24dda --- /dev/null +++ b/src/android/visibility.rs @@ -0,0 +1,12 @@ +use serde::Serialize; + +#[allow(dead_code)] +#[derive(Serialize, Debug)] +#[serde(rename_all = "UPPERCASE")] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#visibility +pub enum Visibility { + VisibilityUnspecified, + Private, + Public, + Secret, +} diff --git a/src/apns/apns_config.rs b/src/apns/apns_config.rs new file mode 100644 index 000000000..db26718fa --- /dev/null +++ b/src/apns/apns_config.rs @@ -0,0 +1,41 @@ +use serde::Serialize; +use serde_json::Value; + +use super::apns_fcm_options::{ApnsFcmOptions, ApnsFcmOptionsInternal}; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#apnsconfig +pub(crate) struct ApnsConfigInternal { + /// HTTP request headers defined in Apple Push Notification Service. + #[serde(skip_serializing_if = "Option::is_none")] + headers: Option, + + /// APNs payload as a JSON object, including both aps dictionary and custom payload. + #[serde(skip_serializing_if = "Option::is_none")] + payload: Option, + + /// Options for features provided by the FCM SDK for iOS. + #[serde(skip_serializing_if = "Option::is_none")] + fcm_options: Option, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#apnsconfig +pub struct ApnsConfig { + /// HTTP request headers defined in Apple Push Notification Service. + pub headers: Option, + /// APNs payload as a JSON object, including both aps dictionary and custom payload. + pub payload: Option, + /// Options for features provided by the FCM SDK for iOS. + pub fcm_options: Option, +} + +impl ApnsConfig { + pub(crate) fn finalize(self) -> ApnsConfigInternal { + ApnsConfigInternal { + headers: self.headers, + payload: self.payload, + fcm_options: self.fcm_options.map(|fcm_options| fcm_options.finalize()), + } + } +} diff --git a/src/apns/apns_fcm_options.rs b/src/apns/apns_fcm_options.rs new file mode 100644 index 000000000..68e41c105 --- /dev/null +++ b/src/apns/apns_fcm_options.rs @@ -0,0 +1,30 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#apnsfcmoptions +pub(crate) struct ApnsFcmOptionsInternal { + /// Label associated with the message's analytics data. + analytics_label: Option, + + /// Contains the URL of an image that is going to be displayed in a notification. + image: Option, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#apnsfcmoptions +pub struct ApnsFcmOptions { + /// Label associated with the message's analytics data. + pub analytics_label: Option, + + /// Contains the URL of an image that is going to be displayed in a notification. + pub image: Option, +} + +impl ApnsFcmOptions { + pub(crate) fn finalize(self) -> ApnsFcmOptionsInternal { + ApnsFcmOptionsInternal { + analytics_label: self.analytics_label, + image: self.image, + } + } +} diff --git a/src/apns/mod.rs b/src/apns/mod.rs index bfd5a2f71..8cd464e28 100644 --- a/src/apns/mod.rs +++ b/src/apns/mod.rs @@ -1,28 +1,2 @@ -use serde::Serialize; -use serde_json::Value; - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#apnsconfig -pub struct ApnsConfig { - // HTTP request headers defined in Apple Push Notification Service. - #[serde(skip_serializing_if = "Option::is_none")] - headers: Option, - - // APNs payload as a JSON object, including both aps dictionary and custom payload. - #[serde(skip_serializing_if = "Option::is_none")] - payload: Option, - - // Options for features provided by the FCM SDK for iOS. - #[serde(skip_serializing_if = "Option::is_none")] - fcm_options: Option, -} - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#apnsfcmoptions -pub struct ApnsFcmOptions { - // Label associated with the message's analytics data. - analytics_label: String, - - // Contains the URL of an image that is going to be displayed in a notification. - image: String, -} +pub mod apns_config; +pub mod apns_fcm_options; diff --git a/src/client/mod.rs b/src/client/mod.rs index 788fda83b..f2418fa87 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,7 +1,7 @@ pub(crate) mod response; use crate::client::response::{ErrorReason, FcmError, FcmResponse, RetryAfter}; -use crate::Message; +use crate::{Message, MessageInternal}; use gauth::serv_account::ServiceAccount; use reqwest::header::RETRY_AFTER; use reqwest::{Body, StatusCode}; @@ -18,9 +18,17 @@ impl Default for Client { } } -#[derive(Serialize, Debug)] -pub struct MessageWrapper { - pub message: Message, +// will be used to wrap the message in a "message" field +#[derive(Serialize)] +struct MessageWrapper<'a> { + #[serde(rename = "message")] + message: &'a MessageInternal, +} + +impl MessageWrapper<'_> { + fn new(message: &MessageInternal) -> MessageWrapper { + MessageWrapper { message } + } } impl Client { @@ -91,7 +99,7 @@ impl Client { Ok(tkn) } - pub async fn access_token(&self) -> Result { + async fn access_token(&self) -> Result { let scopes = vec!["https://www.googleapis.com/auth/firebase.messaging"]; let key_path = self.get_service_key_file_name()?; @@ -101,13 +109,14 @@ impl Client { Err(err) => return Err(err.to_string()), }; - let token_no_bearer = access_token.split(" ").collect::>()[1]; + let token_no_bearer = access_token.split(char::is_whitespace).collect::>()[1]; Ok(token_no_bearer.to_string()) } pub async fn send(&self, message: Message) -> Result { - let wrapper = MessageWrapper { message }; + let fin = message.finalize(); + let wrapper = MessageWrapper::new(&fin); let payload = serde_json::to_vec(&wrapper).unwrap(); let project_id = match self.get_project_id() { diff --git a/src/lib.rs b/src/lib.rs index c44d0a4ba..259f34f97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,66 +9,66 @@ //! To send out a FCM Message with some custom data: //! //! ```no_run -//! # use std::collections::HashMap; -//! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { -//! use fcm::Target; -//! let client = fcm::Client::new(); //! -//! let mut map = HashMap::new(); -//! map.insert("message", "Howdy!"); +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! use serde_json::json; +//! use fcm::{Target, FcmOptions, Notification, Message}; +//! let client = fcm::Client::new(); //! -//! let mut builder = fcm::MessageBuilder::new(Target::Token("token".to_string())); -//! builder.data(&map); +//! let data = json!({ +//! "message": "Howdy!" +//! }); //! -//! let response = client.send(builder.finalize()).await?; -//! println!("Sent: {:?}", response); -//! # Ok(()) -//! # } -//! ``` +//! let builder = Message { +//! data: Some(data), +//! notification: Some(Notification { +//! title: Some("Hello".to_string()), +//! body: Some(format!("it's {}", chrono::Utc::now())), +//! image: None, +//! }), +//! target: Target::Token("token".to_string()), +//! android: None, +//! webpush: None, +//! apns: None, +//! fcm_options: Some(FcmOptions { +//! analytics_label: "analytics_label".to_string(), +//! }), +//! }; //! -//! To send a message using FCM Notifications, we first build the notification: +//! let response = client.send(builder).await?; +//! println!("Sent: {:?}", response); //! -//! ```rust -//! # fn main() { -//! let mut builder = fcm::NotificationBuilder::new(); -//! builder.title("Hey!".to_string()); -//! builder.body("Do you want to catch up later?".to_string()); -//! let notification = builder.finalize(); -//! # } -//! ``` -//! -//! And then set it in the message, before sending it: -//! -//! ```no_run -//! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { -//! use fcm::Target; -//! let client = fcm::Client::new(); -//! -//! let mut notification_builder = fcm::NotificationBuilder::new(); -//! notification_builder.title("Hey!".to_string()); -//! notification_builder.body("Do you want to catch up later?".to_string()); -//! -//! let notification = notification_builder.finalize(); -//! let mut message_builder = fcm::MessageBuilder::new(Target::Token("token".to_string())); -//! message_builder.notification(notification); -//! -//! let response = client.send(message_builder.finalize()).await?; -//! println!("Sent: {:?}", response); -//! # Ok(()) -//! # } +//! Ok(()) +//! } //! ``` mod message; +pub use crate::message::fcm_options::*; +pub use crate::message::target::*; pub use crate::message::*; + mod notification; pub use crate::notification::*; + mod android; +pub use crate::android::android_config::*; +pub use crate::android::android_fcm_options::*; +pub use crate::android::android_message_priority::*; +pub use crate::android::android_notification::*; +pub use crate::android::color::*; +pub use crate::android::light_settings::*; +pub use crate::android::notification_priority::*; +pub use crate::android::visibility::*; + mod apns; -mod client; -mod web; +pub use crate::apns::apns_config::*; +pub use crate::apns::apns_fcm_options::*; -pub use crate::client::*; +mod web; +pub use crate::web::webpush_config::*; +pub use crate::web::webpush_fcm_options::*; +mod client; pub use crate::client::response::FcmError as Error; +pub use crate::client::*; diff --git a/src/message/fcm_options.rs b/src/message/fcm_options.rs new file mode 100644 index 000000000..2a7172f94 --- /dev/null +++ b/src/message/fcm_options.rs @@ -0,0 +1,23 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#fcmoptions +pub(crate) struct FcmOptionsInternal { + /// Label associated with the message's analytics data. + analytics_label: String, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#fcmoptions +pub struct FcmOptions { + /// Label associated with the message's analytics data. + pub analytics_label: String, +} + +impl FcmOptions { + pub(crate) fn finalize(self) -> FcmOptionsInternal { + FcmOptionsInternal { + analytics_label: self.analytics_label, + } + } +} diff --git a/src/message/mod.rs b/src/message/mod.rs index 988073154..6ca73dc91 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,21 +1,26 @@ -use crate::android::AndroidConfig; -use crate::apns::ApnsConfig; -use crate::web::WebpushConfig; -use crate::Notification; -use serde::ser::SerializeMap; -use serde::{Serialize, Serializer}; -use serde_json::Value; +pub mod fcm_options; +pub mod target; #[cfg(test)] mod tests; -#[derive(Clone, Serialize, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Target { - Token(String), - Topic(String), - Condition(String), -} +use serde::ser::SerializeMap; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; + +use crate::android::android_config::AndroidConfig; +use crate::android::android_config::AndroidConfigInternal; +use crate::apns::apns_config::ApnsConfig; +use crate::apns::apns_config::ApnsConfigInternal; +use crate::notification::Notification; +use crate::notification::NotificationInternal; +use crate::web::webpush_config::WebpushConfig; +use crate::web::webpush_config::WebpushConfigInternal; + +use self::fcm_options::FcmOptions; +use self::fcm_options::FcmOptionsInternal; +use self::target::Target; fn output_target(target: &Target, s: S) -> Result where @@ -31,122 +36,67 @@ where } #[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#resource:-message -pub struct Message { - // Arbitrary key/value payload, which must be UTF-8 encoded. +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#resource:-message +pub(crate) struct MessageInternal { + /// Arbitrary key/value payload, which must be UTF-8 encoded. #[serde(skip_serializing_if = "Option::is_none")] data: Option, - // Basic notification template to use across all platforms. + /// Basic notification template to use across all platforms. #[serde(skip_serializing_if = "Option::is_none")] - notification: Option, + notification: Option, - // Android specific options for messages sent through FCM connection server. + /// Android specific options for messages sent through FCM connection server. #[serde(skip_serializing_if = "Option::is_none")] - android: Option, + android: Option, - // Webpush protocol options. + /// Webpush protocol options. #[serde(skip_serializing_if = "Option::is_none")] - webpush: Option, + webpush: Option, - // Apple Push Notification Service specific options. + /// Apple Push Notification Service specific options. #[serde(skip_serializing_if = "Option::is_none")] - apns: Option, + apns: Option, - // Template for FCM SDK feature options to use across all platforms. + /// Template for FCM SDK feature options to use across all platforms. #[serde(skip_serializing_if = "Option::is_none")] - fcm_options: Option, + fcm_options: Option, - // Target to send a message to. + /// Target to send a message to. #[serde(flatten, serialize_with = "output_target")] target: Target, } -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#fcmoptions -pub struct FcmOptions { - // Label associated with the message's analytics data. - analytics_label: String, -} - -/// -/// A builder to get a `Message` instance. -/// -/// # Examples -/// -/// ```rust -/// use fcm::{MessageBuilder, NotificationBuilder, Target}; -/// -/// let mut builder = MessageBuilder::new(Target::Token("token".to_string())); -/// builder.notification(NotificationBuilder::new().finalize()); -/// let message = builder.finalize(); -/// ``` +/// A `Message` instance is the main object to send to the FCM API. +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#resource:-message #[derive(Debug)] -pub struct MessageBuilder { - data: Option, - notification: Option, - target: Target, +pub struct Message { + /// Arbitrary key/value payload, which must be UTF-8 encoded. + pub data: Option, + /// Basic notification template to use across all platforms. + pub notification: Option, + /// Android specific options for messages sent through FCM connection server. + pub target: Target, + /// Webpush protocol options. + pub android: Option, + /// Apple Push Notification Service specific options. + pub webpush: Option, + /// Template for FCM SDK feature options to use across all platforms. + pub apns: Option, + /// Target to send a message to. + pub fcm_options: Option, } -impl MessageBuilder { - /// Get a new instance of Message. You need to supply to. - pub fn new(target: Target) -> Self { - MessageBuilder { - data: None, - notification: None, - target, - } - } - - /// Use this to add custom key-value pairs to the message. This data - /// must be handled appropriately on the client end. The data can be - /// anything that Serde can serialize to JSON. - /// - /// # Examples: - /// ```rust - /// use fcm::{MessageBuilder, Target}; - /// use std::collections::HashMap; - /// - /// let mut map = HashMap::new(); - /// map.insert("message", "Howdy!"); - /// - /// let mut builder = MessageBuilder::new(Target::Token("token".to_string())); - /// builder.data(&map).expect("Should have been able to add data"); - /// let message = builder.finalize(); - /// ``` - pub fn data(&mut self, data: &dyn erased_serde::Serialize) -> Result<&mut Self, serde_json::Error> { - self.data = Some(serde_json::to_value(data)?); - Ok(self) - } - - /// Use this to set a `Notification` for the message. - /// # Examples: - /// ```rust - /// use fcm::{MessageBuilder, NotificationBuilder, Target}; - /// - /// let mut builder = NotificationBuilder::new(); - /// builder.title("Hey!".to_string()); - /// builder.body("Do you want to catch up later?".to_string()); - /// let notification = builder.finalize(); - /// - /// let mut builder = MessageBuilder::new(Target::Token("token".to_string())); - /// builder.notification(notification); - /// let message = builder.finalize(); - /// ``` - pub fn notification(&mut self, notification: Notification) -> &mut Self { - self.notification = Some(notification); - self - } - - /// Complete the build and get a `Message` instance - pub fn finalize(self) -> Message { - Message { +impl Message { + /// Complete the build and get a `MessageInternal` instance + pub(crate) fn finalize(self) -> MessageInternal { + MessageInternal { data: self.data, - notification: self.notification, - android: None, - webpush: None, - apns: None, - fcm_options: None, + notification: self.notification.map(|n| n.finalize()), + android: self.android.map(|a| a.finalize()), + webpush: self.webpush.map(|w| w.finalize()), + apns: self.apns.map(|a| a.finalize()), + fcm_options: self.fcm_options.map(|f| f.finalize()), target: self.target, } } diff --git a/src/message/target.rs b/src/message/target.rs new file mode 100644 index 000000000..b1af04f3f --- /dev/null +++ b/src/message/target.rs @@ -0,0 +1,18 @@ +use serde::Serialize; + +/// Target to send a message to. +/// +/// ```rust +/// use fcm::{Target}; +/// +/// Target::Token("myfcmtoken".to_string()); +/// Target::Topic("my-topic-name".to_string()); +/// Target::Condition("my-condition".to_string()); +/// ``` +#[derive(Clone, Serialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Target { + Token(String), + Topic(String), + Condition(String), +} diff --git a/src/message/tests.rs b/src/message/tests.rs index c23e94e45..8cb9fb713 100644 --- a/src/message/tests.rs +++ b/src/message/tests.rs @@ -1,18 +1,19 @@ -use crate::notification::NotificationBuilder; -use crate::{MessageBuilder, Target}; -use serde::Serialize; +use crate::{message::Target, notification::Notification, Message}; use serde_json::json; -#[derive(Serialize)] -struct CustomData { - foo: &'static str, - bar: bool, -} - #[test] fn should_create_new_message() { let target = Target::Token("token".to_string()); - let msg = MessageBuilder::new(target.clone()).finalize(); + let msg = Message { + target: target.clone(), + data: None, + notification: None, + android: None, + webpush: None, + apns: None, + fcm_options: None, + } + .finalize(); assert_eq!(msg.target, target); } @@ -20,7 +21,16 @@ fn should_create_new_message() { #[test] fn should_leave_nones_out_of_the_json() { let target = Target::Token("token".to_string()); - let msg = MessageBuilder::new(target).finalize(); + let msg = Message { + target: target.clone(), + data: None, + notification: None, + android: None, + webpush: None, + apns: None, + fcm_options: None, + } + .finalize(); let payload = serde_json::to_string(&msg).unwrap(); let expected_payload = json!({ @@ -34,11 +44,17 @@ fn should_leave_nones_out_of_the_json() { #[test] fn should_add_custom_data_to_the_payload() { let target = Target::Token("token".to_string()); - let mut builder = MessageBuilder::new(target); - - let data = CustomData { foo: "bar", bar: false }; - - builder.data(&data).unwrap(); + let data = json!({ "foo": "bar", "bar": false }); + + let builder = Message { + target: target, + data: Some(data), + notification: None, + android: None, + webpush: None, + apns: None, + fcm_options: None, + }; let msg = builder.finalize(); let payload = serde_json::to_string(&msg).unwrap(); @@ -58,9 +74,20 @@ fn should_add_custom_data_to_the_payload() { #[test] fn should_be_able_to_render_a_full_token_message_to_json() { let target = Target::Token("token".to_string()); - let mut builder = MessageBuilder::new(target); - - builder.notification(NotificationBuilder::new().finalize()); + let notification = Notification { + title: None, + body: None, + image: None, + }; + let builder = Message { + target: target.clone(), + data: None, + notification: Some(notification), + android: None, + webpush: None, + apns: None, + fcm_options: None, + }; let payload = serde_json::to_string(&builder.finalize()).unwrap(); @@ -76,9 +103,20 @@ fn should_be_able_to_render_a_full_token_message_to_json() { #[test] fn should_be_able_to_render_a_full_topic_message_to_json() { let target = Target::Topic("my_topic".to_string()); - let mut builder = MessageBuilder::new(target); - - builder.notification(NotificationBuilder::new().finalize()); + let notification = Notification { + title: None, + body: None, + image: None, + }; + let builder = Message { + target: target.clone(), + data: None, + notification: Some(notification), + android: None, + webpush: None, + apns: None, + fcm_options: None, + }; let payload = serde_json::to_string(&builder.finalize()).unwrap(); @@ -94,9 +132,20 @@ fn should_be_able_to_render_a_full_topic_message_to_json() { #[test] fn should_be_able_to_render_a_full_condition_message_to_json() { let target = Target::Condition("my_condition".to_string()); - let mut builder = MessageBuilder::new(target); - - builder.notification(NotificationBuilder::new().finalize()); + let notification = Notification { + title: None, + body: None, + image: None, + }; + let builder = Message { + target: target.clone(), + data: None, + notification: Some(notification), + android: None, + webpush: None, + apns: None, + fcm_options: None, + }; let payload = serde_json::to_string(&builder.finalize()).unwrap(); @@ -112,15 +161,23 @@ fn should_be_able_to_render_a_full_condition_message_to_json() { #[test] fn should_set_notifications() { let target = Target::Token("token".to_string()); - let msg = MessageBuilder::new(target.clone()).finalize(); - - assert_eq!(msg.notification, None); - - let nm = NotificationBuilder::new().finalize(); - let mut builder = MessageBuilder::new(target); - builder.notification(nm); + let nm = Notification { + title: None, + body: None, + image: None, + }; + + let builder = Message { + target: target.clone(), + data: None, + notification: Some(nm), + android: None, + webpush: None, + apns: None, + fcm_options: None, + }; let msg = builder.finalize(); - assert_ne!(msg.notification, None); + assert_eq!(msg.notification.is_none(), false); } diff --git a/src/notification/mod.rs b/src/notification/mod.rs index 0ca5606a0..d76d7ed28 100644 --- a/src/notification/mod.rs +++ b/src/notification/mod.rs @@ -1,73 +1,43 @@ -use serde::Serialize; - #[cfg(test)] mod tests; +use serde::Serialize; + /// This struct represents a FCM notification. Use the -/// corresponding `NotificationBuilder` to get an instance. You can then use +/// corresponding `Notification` to get an instance. You can then use /// this notification instance when sending a FCM message. #[derive(Serialize, Debug, PartialEq)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#notification -pub struct Notification { - // The notification's title. +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#notification +pub(crate) struct NotificationInternal { + /// The notification's title. #[serde(skip_serializing_if = "Option::is_none")] title: Option, - // The notification's body text. + /// The notification's body text. #[serde(skip_serializing_if = "Option::is_none")] body: Option, - // Contains the URL of an image that is going to be downloaded on the device and displayed in a notification. + /// Contains the URL of an image that is going to be downloaded on the device and displayed in a notification. #[serde(skip_serializing_if = "Option::is_none")] image: Option, } -/// A builder to get a `Notification` instance. -/// -/// # Examples -/// -/// ```rust -/// use fcm::NotificationBuilder; -/// -/// let mut builder = NotificationBuilder::new(); -/// builder.title("Australia vs New Zealand".to_string()); -/// builder.body("3 runs to win in 1 ball".to_string()); -/// let notification = builder.finalize(); -/// ``` -#[derive(Default)] -pub struct NotificationBuilder { - title: Option, - body: Option, - image: Option, -} - -impl NotificationBuilder { - /// Get a new `NotificationBuilder` instance, with a title. - pub fn new() -> NotificationBuilder { - Self::default() - } - - // Set the title of the notification - pub fn title(&mut self, title: String) -> &mut Self { - self.title = Some(title); - self - } +#[derive(Debug, Default)] +pub struct Notification { + /// The notification's title. + pub title: Option, - /// Set the body of the notification - pub fn body(&mut self, body: String) -> &mut Self { - self.body = Some(body); - self - } + /// The notification's body text. + pub body: Option, - /// Set the image - pub fn image(&mut self, image: String) -> &mut Self { - self.image = Some(image); - self - } + /// Contains the URL of an image that is going to be downloaded on the device and displayed in a notification. + pub image: Option, +} +impl Notification { /// Complete the build and get a `Notification` instance - pub fn finalize(self) -> Notification { - Notification { + pub(crate) fn finalize(self) -> NotificationInternal { + NotificationInternal { title: self.title, body: self.body, image: self.image, diff --git a/src/notification/tests.rs b/src/notification/tests.rs index 36315ec1a..3bf593d87 100644 --- a/src/notification/tests.rs +++ b/src/notification/tests.rs @@ -1,16 +1,15 @@ -use crate::NotificationBuilder; +use crate::Notification; use serde_json::json; #[test] fn should_be_able_to_render_a_full_notification_to_json() { - let mut builder = NotificationBuilder::new(); + let not = Notification { + title: Some("foo".to_string()), + body: Some("bar".to_string()), + image: Some("https://my.image.com/test.jpg".to_string()), + }; - builder - .title("foo".to_string()) - .body("bar".to_string()) - .image("https://my.image.com/test.jpg".to_string()); - - let payload = serde_json::to_string(&builder.finalize()).unwrap(); + let payload = serde_json::to_string(¬.finalize()).unwrap(); let expected_payload = json!({ "title": "foo", @@ -21,38 +20,3 @@ fn should_be_able_to_render_a_full_notification_to_json() { assert_eq!(expected_payload, payload); } - -#[test] -fn should_set_notification_title() { - let nm = NotificationBuilder::new().finalize(); - - assert_eq!(nm.title, None); - - let mut builder = NotificationBuilder::new(); - builder.title("title".to_string()); - let nm = builder.finalize(); - - assert_eq!(nm.title, Some("title".to_string())); -} - -#[test] -fn should_set_notification_body() { - let nm = NotificationBuilder::new().finalize(); - - assert_eq!(nm.body, None); - - let mut builder = NotificationBuilder::new(); - builder.body("body".to_string()); - let nm = builder.finalize(); - - assert_eq!(nm.body, Some("body".to_string())); -} - -#[test] -fn should_set_notification_image() { - let mut builder = NotificationBuilder::new(); - builder.image("https://my.image.com/test.jpg".to_string()); - let nm = builder.finalize(); - - assert_eq!(nm.image, Some("https://my.image.com/test.jpg".to_string())); -} diff --git a/src/web/mod.rs b/src/web/mod.rs index 60d42990d..52e1762c8 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,33 +1,2 @@ -use serde::Serialize; -use serde_json::Value; - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#webpushconfig -pub struct WebpushConfig { - // HTTP headers defined in webpush protocol. - #[serde(skip_serializing_if = "Option::is_none")] - headers: Option, - - // Arbitrary key/value payload. - #[serde(skip_serializing_if = "Option::is_none")] - data: Option, - - // Web Notification options as a JSON object. - // Struct format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Struct - #[serde(skip_serializing_if = "Option::is_none")] - notification: Option, - - // Options for features provided by the FCM SDK for Web. - #[serde(skip_serializing_if = "Option::is_none")] - fcm_options: Option, -} - -#[derive(Serialize, Debug)] -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#webpushfcmoptions -pub struct WebpushFcmOptions { - // The link to open when the user clicks on the notification. - link: String, - - // Label associated with the message's analytics data. - analytics_label: String, -} +pub mod webpush_config; +pub mod webpush_fcm_options; diff --git a/src/web/webpush_config.rs b/src/web/webpush_config.rs new file mode 100644 index 000000000..85f1757ff --- /dev/null +++ b/src/web/webpush_config.rs @@ -0,0 +1,53 @@ +use serde::Serialize; +use serde_json::Value; + +use super::webpush_fcm_options::{WebpushFcmOptions, WebpushFcmOptionsInternal}; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#webpushconfig +pub(crate) struct WebpushConfigInternal { + /// HTTP headers defined in webpush protocol. + #[serde(skip_serializing_if = "Option::is_none")] + headers: Option, + + /// Arbitrary key/value payload. + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, + + /// Web Notification options as a JSON object. + /// Struct format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Struct + #[serde(skip_serializing_if = "Option::is_none")] + notification: Option, + + /// Options for features provided by the FCM SDK for Web. + #[serde(skip_serializing_if = "Option::is_none")] + fcm_options: Option, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#webpushconfig +pub struct WebpushConfig { + /// HTTP headers defined in webpush protocol. + pub headers: Option, + + /// Arbitrary key/value payload. + pub data: Option, + + /// Web Notification options as a JSON object. + /// Struct format: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf?authuser=0#google.protobuf.Struct + pub notification: Option, + + /// Options for features provided by the FCM SDK for Web. + pub fcm_options: Option, +} + +impl WebpushConfig { + pub(crate) fn finalize(self) -> WebpushConfigInternal { + WebpushConfigInternal { + headers: self.headers, + data: self.data, + notification: self.notification, + fcm_options: self.fcm_options.map(|fcm_options| fcm_options.finalize()), + } + } +} diff --git a/src/web/webpush_fcm_options.rs b/src/web/webpush_fcm_options.rs new file mode 100644 index 000000000..56ceb05ab --- /dev/null +++ b/src/web/webpush_fcm_options.rs @@ -0,0 +1,30 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#webpushfcmoptions +pub(crate) struct WebpushFcmOptionsInternal { + /// The link to open when the user clicks on the notification. + link: String, + + /// Label associated with the message's analytics data. + analytics_label: String, +} + +#[derive(Debug, Default)] +/// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#webpushfcmoptions +pub struct WebpushFcmOptions { + /// The link to open when the user clicks on the notification. + pub link: String, + + /// Label associated with the message's analytics data. + pub analytics_label: String, +} + +impl WebpushFcmOptions { + pub(crate) fn finalize(self) -> WebpushFcmOptionsInternal { + WebpushFcmOptionsInternal { + link: self.link, + analytics_label: self.analytics_label, + } + } +}