1use gcp_auth::CustomServiceAccount;
2use gcp_auth::TokenProvider;
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6use serde_json::Value;
7use std::error::Error;
8use std::fs;
9use std::io;
10use std::path::PathBuf;
11mod domain;
12pub use domain::AndroidConfig;
13pub use domain::ApnsConfig;
14pub use domain::FcmMessage;
15pub use domain::FcmNotification;
16pub use domain::FcmOptions;
17pub use domain::Target;
18pub use domain::WebpushConfig;
19
20#[derive(Serialize, Deserialize)]
22pub struct FcmPayload {
23 pub message: FcmMessage,
24}
25
26pub struct FcmService {
27 pub credential_file: String,
28}
29
30impl FcmService {
31 pub fn new(credential_file: impl Into<String>) -> Self {
32 Self {
33 credential_file: credential_file.into(),
34 }
35 }
36}
37
38impl FcmService {
64 fn get_project_id(&self) -> Result<String, Box<dyn Error>> {
66 let content = fs::read_to_string(&self.credential_file)?;
67 let json: Value = serde_json::from_str(&content)?;
68
69 json.get("project_id")
70 .and_then(|v| v.as_str())
71 .map(|s| s.to_string())
72 .ok_or_else(|| {
73 io::Error::new(io::ErrorKind::InvalidData, "project_id not found").into()
74 })
75 }
76 pub async fn send_notification(&self, message: FcmMessage) -> Result<(), Box<dyn Error>> {
85 let project_id = self.get_project_id()?;
86 let client = Client::new();
87 let credentials_path = PathBuf::from(&self.credential_file);
88 let service_account = CustomServiceAccount::from_file(credentials_path)?;
90 let scopes = &["https://www.googleapis.com/auth/firebase.messaging"];
91 let token = service_account.token(scopes).await?;
92 let url = format!(
93 "https://fcm.googleapis.com/v1/projects/{}/messages:send",
94 project_id
95 );
96
97 let payload = FcmPayload { message };
98
99 let response = client
100 .post(&url)
101 .bearer_auth(token.as_str())
102 .json(&payload)
103 .send()
104 .await?;
105
106 if response.status().is_success() {
107 let response_text = response.text().await?;
108 println!("Notification sent successfully: {}", response_text);
109 println!("Notification Payload is : {:#?}", json!(payload));
110
111 Ok(())
112 } else {
113 let error_text = response.text().await?;
114 Err(format!("Failed to send notification: {:#?}", error_text).into())
115 }
116 }
117}
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::fs::File;
122 use std::io::Write;
123 use tempfile;
124
125 fn setup_dummy_credentials(temp_dir: &tempfile::TempDir) -> String {
126 let credential_path = temp_dir.path().join("service-account.json");
127 let mut file = File::create(&credential_path).unwrap();
128 writeln!(
129 file,
130 r#"{{"project_id": "test-project", "client_email": "test@example.com"}}"#
131 )
132 .unwrap();
133 credential_path.to_str().unwrap().to_string()
134 }
135
136 #[test]
137 fn test_new_service() {
138 let service = FcmService::new("dummy.json");
139 assert_eq!(service.credential_file, "dummy.json");
140 }
141
142 #[test]
143 fn test_get_project_id_success() {
144 let temp_dir = tempfile::tempdir().unwrap();
145 let credential_file = setup_dummy_credentials(&temp_dir);
146 let service = FcmService::new(credential_file);
147 let project_id = service.get_project_id().unwrap();
148 assert_eq!(project_id, "test-project");
149 }
150
151 #[test]
152 fn test_get_project_id_missing_file() {
153 let service = FcmService::new("nonexistent.json");
154 let result = service.get_project_id();
155 assert!(result.is_err());
156 assert!(matches!(
157 result.unwrap_err().downcast_ref::<io::Error>(),
158 Some(err) if err.kind() == io::ErrorKind::NotFound
159 ));
160 }
161
162 #[test]
163 fn test_get_project_id_invalid_json() {
164 let temp_dir = tempfile::tempdir().unwrap();
165 let credential_path = temp_dir.path().join("service-account.json");
166 let mut file = File::create(&credential_path).unwrap();
167 writeln!(file, "invalid json").unwrap();
168 let service = FcmService::new(credential_path.to_str().unwrap());
169 let result = service.get_project_id();
170 assert!(result.is_err());
171 }
172}