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