Skip to main content

fcm_notification/
lib.rs

1//! A Rust library for sending Firebase Cloud Messaging (FCM) notifications.
2//!
3//! This crate provides a simple interface to send push notifications using Firebase Cloud Messaging (FCM).
4//! It handles authentication with Google OAuth2 and constructs the necessary payloads for FCM requests.
5//!
6//! # Example
7//! ```rust
8//! use fcm_notification::{FcmNotification, NotificationPayload};
9//!
10//! #[tokio::main]
11//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
12//!     let fcm = FcmNotification::new("service_account.json")?;
13//!     let notification = NotificationPayload {
14//!         token: "device-token-here",
15//!         title: "New Like",
16//!         body: "Someone liked your post!",
17//!         data: None,
18//!     };
19//!     fcm.send_notification(&notification).await?;
20//!     Ok(())
21//! }
22//! ```
23
24use chrono::Utc;
25use jsonwebtoken::{encode, EncodingKey, Header};
26use reqwest::Client;
27use serde::{Deserialize, Serialize};
28use serde_json::json;
29use std::fs;
30use thiserror::Error;
31
32/// Represents a Firebase service account, loaded from a JSON file.
33///
34/// This struct is used to store the credentials required to authenticate with Google OAuth2
35/// and send FCM notifications.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ServiceAccount {
38    #[serde(rename = "type")]
39    pub account_type: String,
40    pub project_id: String,
41    pub private_key_id: String,
42    pub private_key: String,
43    pub client_email: String,
44    pub client_id: String,
45    pub auth_uri: String,
46    pub token_uri: String,
47    pub auth_provider_x509_cert_url: String,
48    pub client_x509_cert_url: String,
49    pub universe_domain: String,
50}
51
52/// Represents the payload for an FCM notification.
53///
54/// This struct is used to define the content of the notification, including the target device token,
55/// the title, the body, and optional additional data.
56#[derive(Debug, Serialize)]
57pub struct NotificationPayload<'a> {
58    /// The device token of the target device.
59    pub token: &'a str,
60    /// The title of the notification.
61    pub title: &'a str,
62    /// The body of the notification.
63    pub body: &'a str,
64    /// Optional additional data to include in the notification.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub data: Option<serde_json::Value>,
67}
68
69/// Represents errors that can occur while using the `FcmNotification`.
70///
71/// This enum provides a unified error type for all operations, including file I/O, JSON parsing,
72/// JWT encoding, HTTP requests, and FCM-specific errors.
73#[derive(Debug, Error)]
74pub enum FcmError {
75    #[error("Failed to read service account file: {0}")]
76    FileReadError(#[from] std::io::Error),
77    #[error("Failed to parse service account JSON: {0}")]
78    JsonParseError(#[from] serde_json::Error),
79    #[error("Failed to encode JWT: {0}")]
80    JwtEncodeError(#[from] jsonwebtoken::errors::Error),
81    #[error("Failed to send HTTP request: {0}")]
82    HttpError(#[from] reqwest::Error),
83    #[error("Access token not found in response")]
84    AccessTokenNotFound,
85    #[error("Failed to send notification: {0}")]
86    NotificationError(String),
87}
88
89/// The main service for sending FCM notifications.
90///
91/// This struct provides methods to authenticate with Google OAuth2 and send notifications
92/// using the Firebase Cloud Messaging API.
93#[derive(Clone)]
94pub struct FcmNotification {
95    service_account: ServiceAccount,
96    client: Client,
97}
98
99impl FcmNotification {
100    /// Creates a new `FcmNotification` instance.
101    ///
102    /// # Arguments
103    /// * `config_path` - The path to the Firebase service account JSON file.
104    ///
105    /// # Errors
106    /// Returns an error if the file cannot be read or the JSON cannot be parsed.
107    pub fn new(config_path: &str) -> Result<Self, FcmError> {
108        let config_file = fs::read_to_string(config_path)?;
109        let service_account: ServiceAccount = serde_json::from_str(&config_file)?;
110
111        Ok(Self {
112            service_account,
113            client: Client::new(),
114        })
115    }
116
117    /// Generates an OAuth2 access token using the service account credentials.
118    ///
119    /// This method creates a JWT (JSON Web Token) and exchanges it for an access token
120    /// using the Google OAuth2 token endpoint.
121    ///
122    /// # Errors
123    /// Returns an error if the JWT cannot be encoded or the HTTP request fails.
124    async fn get_access_token(&self) -> Result<String, FcmError> {
125        #[derive(Serialize)]
126        struct Claims {
127            iss: String,
128            scope: String,
129            aud: String,
130            exp: i64,
131            iat: i64,
132        }
133
134        let now = Utc::now();
135        let claims = Claims {
136            iss: self.service_account.client_email.clone(),
137            scope: "https://www.googleapis.com/auth/firebase.messaging".to_string(),
138            aud: "https://oauth2.googleapis.com/token".to_string(),
139            exp: (now + chrono::Duration::hours(1)).timestamp(),
140            iat: now.timestamp(),
141        };
142
143        let encoding_key = EncodingKey::from_rsa_pem(self.service_account.private_key.as_bytes())?;
144        let jwt = encode(
145            &Header::new(jsonwebtoken::Algorithm::RS256),
146            &claims,
147            &encoding_key,
148        )?;
149
150        let params = [
151            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
152            ("assertion", &jwt),
153        ];
154
155        let response = self
156            .client
157            .post("https://oauth2.googleapis.com/token")
158            .form(&params)
159            .send()
160            .await?
161            .json::<serde_json::Value>()
162            .await?;
163
164        let access_token = response["access_token"]
165            .as_str()
166            .ok_or(FcmError::AccessTokenNotFound)?
167            .to_string();
168
169        Ok(access_token)
170    }
171
172    /// Sends an FCM notification to the specified device.
173    ///
174    /// # Arguments
175    /// * `notification` - The notification payload containing the device token, title, body, and optional data.
176    ///
177    /// # Errors
178    /// Returns an error if the access token cannot be retrieved or the HTTP request fails.
179    pub async fn send_notification(
180        &self,
181        notification: &NotificationPayload<'_>,
182    ) -> Result<(), FcmError> {
183        let access_token = self.get_access_token().await?;
184
185        let notification_payload = json!({
186            "message": {
187                "token": notification.token,
188                "notification": {
189                    "title": notification.title,
190                    "body": notification.body
191                },
192                "data": notification.data
193            }
194        });
195
196        let url = format!(
197            "https://fcm.googleapis.com/v1/projects/{}/messages:send",
198            self.service_account.project_id
199        );
200
201        let response = self
202            .client
203            .post(&url)
204            .header("Authorization", format!("Bearer {}", access_token))
205            .header("Content-Type", "application/json")
206            .json(&notification_payload)
207            .send()
208            .await?;
209
210        if response.status().is_success() {
211            println!("Notification sent successfully");
212            Ok(())
213        } else {
214            Err(FcmError::NotificationError(response.text().await?))
215        }
216    }
217}