use crate::service_account_key::ServiceAccountKey;
use anyhow::anyhow;
use chrono::Utc;
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use tokio::sync::RwLock;
pub struct FirebaseClient {
pub service_account_key: ServiceAccountKey,
pub reqwest_client: reqwest::Client,
pub token_response: Arc<RwLock<Option<TokenResponse>>>,
pub token_expiry: AtomicI64,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct TokenResponse {
pub access_token: String,
pub expires_in: i64,
pub token_type: String,
}
impl FirebaseClient {
pub fn new_from_file(path: &str) -> anyhow::Result<Self> {
let service_account_key = ServiceAccountKey::from_file(path)?;
let reqwest_client = reqwest::Client::new();
Ok(Self {
service_account_key,
reqwest_client,
token_response: Arc::new(RwLock::new(None)),
token_expiry: AtomicI64::new(0),
})
}
pub fn new_from_env(app_env: Option<String>) -> anyhow::Result<Self> {
let service_account_key = ServiceAccountKey::from_env(app_env)?;
let reqwest_client = reqwest::Client::new();
Ok(Self {
service_account_key,
reqwest_client,
token_response: Arc::new(RwLock::new(None)),
token_expiry: AtomicI64::new(0),
})
}
pub fn create_jwt(&self, scopes: &str) -> anyhow::Result<String> {
let now = Utc::now().timestamp();
let claims = json!({
"iss": self.service_account_key.client_email,
"scope": scopes,
"aud": self.service_account_key.token_uri,
"iat": now,
"exp": now + 3600,
});
let mut header = Header::new(Algorithm::RS256);
header.kid = Some(self.service_account_key.private_key_id.clone());
header.typ = Some("JWT".to_string());
let encoding_key =
EncodingKey::from_rsa_pem(self.service_account_key.private_key.as_bytes())?;
let jwt = encode(&header, &claims, &encoding_key)?;
self.token_expiry.store(now + 3600, Ordering::Relaxed);
Ok(jwt)
}
pub async fn get_access_token(&self, scopes: &str) -> anyhow::Result<()> {
let jwt = self.create_jwt(scopes)?;
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("assertion", &jwt),
];
let res = self
.reqwest_client
.post(&self.service_account_key.token_uri)
.form(¶ms)
.send()
.await?
.json::<TokenResponse>()
.await?;
let mut token_response = self.token_response.write().await;
*token_response = Some(res);
Ok(())
}
pub async fn send_message(&self, message: &impl Serialize) -> anyhow::Result<StatusCode> {
let now = Utc::now().timestamp();
if now > self.token_expiry.load(Ordering::Relaxed) {
let scopes = "https://www.googleapis.com/auth/firebase.messaging";
self.get_access_token(scopes).await?;
}
let token_response = self
.token_response
.read()
.await
.clone()
.ok_or(anyhow!("No token response"))?;
let url = format!(
"https://fcm.googleapis.com/v1/projects/{}/messages:send",
self.service_account_key.project_id
);
let res = self
.reqwest_client
.post(&url)
.bearer_auth(token_response.access_token)
.json(&json!({"message": message}))
.send()
.await?;
println!("res {:?}", res);
Ok(res.status())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::{Notification, TopicMessage};
#[tokio::test]
pub async fn token_test() {
let firebase_client = FirebaseClient::new_from_env(None).unwrap();
let scopes = "https://www.googleapis.com/auth/firebase.messaging";
let token_response = firebase_client.get_access_token(&scopes).await;
assert!(token_response.is_ok());
}
#[tokio::test]
pub async fn topic_message_test() {
let firebase_client = FirebaseClient::new_from_env(None).unwrap();
let message = TopicMessage {
topic: "test".to_string(),
notification: Some(Notification {
title: "title".to_string(),
body: "body".to_string(),
}),
data: None,
};
let message_response = firebase_client.send_message(&message).await;
assert!(message_response.is_ok());
}
}