use chrono::Utc;
use jsonwebtoken::{encode, EncodingKey, Header};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fs;
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceAccount {
#[serde(rename = "type")]
pub account_type: String,
pub project_id: String,
pub private_key_id: String,
pub private_key: String,
pub client_email: String,
pub client_id: String,
pub auth_uri: String,
pub token_uri: String,
pub auth_provider_x509_cert_url: String,
pub client_x509_cert_url: String,
pub universe_domain: String,
}
#[derive(Debug, Serialize)]
pub struct NotificationPayload<'a> {
pub token: &'a str,
pub title: &'a str,
pub body: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Error)]
pub enum FcmError {
#[error("Failed to read service account file: {0}")]
FileReadError(#[from] std::io::Error),
#[error("Failed to parse service account JSON: {0}")]
JsonParseError(#[from] serde_json::Error),
#[error("Failed to encode JWT: {0}")]
JwtEncodeError(#[from] jsonwebtoken::errors::Error),
#[error("Failed to send HTTP request: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Access token not found in response")]
AccessTokenNotFound,
#[error("Failed to send notification: {0}")]
NotificationError(String),
}
#[derive(Clone)]
pub struct FcmNotification {
service_account: ServiceAccount,
client: Client,
}
impl FcmNotification {
pub fn new(config_path: &str) -> Result<Self, FcmError> {
let config_file = fs::read_to_string(config_path)?;
let service_account: ServiceAccount = serde_json::from_str(&config_file)?;
Ok(Self {
service_account,
client: Client::new(),
})
}
async fn get_access_token(&self) -> Result<String, FcmError> {
#[derive(Serialize)]
struct Claims {
iss: String,
scope: String,
aud: String,
exp: i64,
iat: i64,
}
let now = Utc::now();
let claims = Claims {
iss: self.service_account.client_email.clone(),
scope: "https://www.googleapis.com/auth/firebase.messaging".to_string(),
aud: "https://oauth2.googleapis.com/token".to_string(),
exp: (now + chrono::Duration::hours(1)).timestamp(),
iat: now.timestamp(),
};
let encoding_key = EncodingKey::from_rsa_pem(self.service_account.private_key.as_bytes())?;
let jwt = encode(
&Header::new(jsonwebtoken::Algorithm::RS256),
&claims,
&encoding_key,
)?;
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("assertion", &jwt),
];
let response = self
.client
.post("https://oauth2.googleapis.com/token")
.form(¶ms)
.send()
.await?
.json::<serde_json::Value>()
.await?;
let access_token = response["access_token"]
.as_str()
.ok_or(FcmError::AccessTokenNotFound)?
.to_string();
Ok(access_token)
}
pub async fn send_notification(
&self,
notification: &NotificationPayload<'_>,
) -> Result<(), FcmError> {
let access_token = self.get_access_token().await?;
let notification_payload = json!({
"message": {
"token": notification.token,
"notification": {
"title": notification.title,
"body": notification.body
},
"data": notification.data
}
});
let url = format!(
"https://fcm.googleapis.com/v1/projects/{}/messages:send",
self.service_account.project_id
);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.json(¬ification_payload)
.send()
.await?;
if response.status().is_success() {
println!("Notification sent successfully");
Ok(())
} else {
Err(FcmError::NotificationError(response.text().await?))
}
}
}