1use 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#[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#[derive(Debug, Serialize)]
57pub struct NotificationPayload<'a> {
58 pub token: &'a str,
60 pub title: &'a str,
62 pub body: &'a str,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub data: Option<serde_json::Value>,
67}
68
69#[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#[derive(Clone)]
94pub struct FcmNotification {
95 service_account: ServiceAccount,
96 client: Client,
97}
98
99impl FcmNotification {
100 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 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(¶ms)
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 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(¬ification_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}