firebase_sdk_rs/
firebase_client.rs1use 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(¶ms)
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}