Skip to main content

firebase_sdk_rs/
firebase_client.rs

1use crate::service_account_key::ServiceAccountKey;
2use anyhow::anyhow;
3use chrono::Utc;
4use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
5use reqwest::StatusCode;
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicI64, Ordering};
10use tokio::sync::RwLock;
11
12pub struct FirebaseClient {
13    pub service_account_key: ServiceAccountKey,
14    pub reqwest_client: reqwest::Client,
15    pub token_response: Arc<RwLock<Option<TokenResponse>>>,
16    pub token_expiry: AtomicI64,
17}
18
19#[derive(Debug, Deserialize, Serialize, Clone)]
20pub struct TokenResponse {
21    pub access_token: String,
22    pub expires_in: i64,
23    pub token_type: String,
24}
25
26impl FirebaseClient {
27    pub fn new_from_file(path: &str) -> anyhow::Result<Self> {
28        let service_account_key = ServiceAccountKey::from_file(path)?;
29        let reqwest_client = reqwest::Client::new();
30        Ok(Self {
31            service_account_key,
32            reqwest_client,
33            token_response: Arc::new(RwLock::new(None)),
34            token_expiry: AtomicI64::new(0),
35        })
36    }
37
38    pub fn new_from_env(app_env: Option<String>) -> anyhow::Result<Self> {
39        let service_account_key = ServiceAccountKey::from_env(app_env)?;
40        let reqwest_client = reqwest::Client::new();
41        Ok(Self {
42            service_account_key,
43            reqwest_client,
44            token_response: Arc::new(RwLock::new(None)),
45            token_expiry: AtomicI64::new(0),
46        })
47    }
48
49    pub fn create_jwt(&self, scopes: &str) -> anyhow::Result<String> {
50        let now = Utc::now().timestamp();
51        let claims = json!({
52            "iss": self.service_account_key.client_email,
53            "scope": scopes,
54            "aud": self.service_account_key.token_uri,
55            "iat": now,
56            "exp": now + 3600,
57        });
58
59        let mut header = Header::new(Algorithm::RS256);
60        header.kid = Some(self.service_account_key.private_key_id.clone());
61        header.typ = Some("JWT".to_string());
62        let encoding_key =
63            EncodingKey::from_rsa_pem(self.service_account_key.private_key.as_bytes())?;
64        let jwt = encode(&header, &claims, &encoding_key)?;
65        self.token_expiry.store(now + 3600, Ordering::Relaxed);
66        Ok(jwt)
67    }
68
69    pub async fn get_access_token(&self, scopes: &str) -> anyhow::Result<()> {
70        let jwt = self.create_jwt(scopes)?;
71        let params = [
72            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
73            ("assertion", &jwt),
74        ];
75        let res = self
76            .reqwest_client
77            .post(&self.service_account_key.token_uri)
78            .form(&params)
79            .send()
80            .await?
81            .json::<TokenResponse>()
82            .await?;
83        let mut token_response = self.token_response.write().await;
84        *token_response = Some(res);
85        Ok(())
86    }
87
88    pub async fn send_message(&self, message: &impl Serialize) -> anyhow::Result<StatusCode> {
89        let now = Utc::now().timestamp();
90        if now > self.token_expiry.load(Ordering::Relaxed) {
91            let scopes = "https://www.googleapis.com/auth/firebase.messaging";
92            self.get_access_token(scopes).await?;
93        }
94
95        let token_response = self
96            .token_response
97            .read()
98            .await
99            .clone()
100            .ok_or(anyhow!("No token response"))?;
101
102        let url = format!(
103            "https://fcm.googleapis.com/v1/projects/{}/messages:send",
104            self.service_account_key.project_id
105        );
106
107        let res = self
108            .reqwest_client
109            .post(&url)
110            .bearer_auth(token_response.access_token)
111            .json(&json!({"message": message}))
112            .send()
113            .await?;
114        println!("res {:?}", res);
115        Ok(res.status())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::message::{Notification, TopicMessage};
123    #[tokio::test]
124    pub async fn token_test() {
125        let firebase_client = FirebaseClient::new_from_env(None).unwrap();
126        let scopes = "https://www.googleapis.com/auth/firebase.messaging";
127        let token_response = firebase_client.get_access_token(&scopes).await;
128        assert!(token_response.is_ok());
129    }
130
131    #[tokio::test]
132    pub async fn topic_message_test() {
133        let firebase_client = FirebaseClient::new_from_env(None).unwrap();
134        let message = TopicMessage {
135            topic: "test".to_string(),
136            notification: Some(Notification {
137                title: "title".to_string(),
138                body: "body".to_string(),
139            }),
140            data: None,
141        };
142        let message_response = firebase_client.send_message(&message).await;
143        assert!(message_response.is_ok());
144    }
145}