use std::{error::Error, fs, io, path::PathBuf};
use gcp_auth::{CustomServiceAccount, TokenProvider};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
mod domain;
pub use domain::{
AndroidConfig, AndroidNotification, ApnsConfig, Color, FcmMessage, FcmNotification, FcmOptions,
LightSettings, NotificationPriority, Priority, Proxy, Target, Visibility, WebpushConfig,
};
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct FcmPayload {
pub message: FcmMessage,
}
pub struct FcmService {
pub credential_file: String,
}
impl FcmService {
pub fn new(credential_file: impl Into<String>) -> Self {
Self {
credential_file: credential_file.into(),
}
}
}
impl FcmService {
fn get_project_id(&self) -> Result<String, Box<dyn Error>> {
let content = fs::read_to_string(&self.credential_file)?;
let json: Value = serde_json::from_str(&content)?;
json.get("project_id")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string)
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "project_id not found").into()
})
}
pub async fn send_notification(&self, message: FcmMessage) -> Result<(), Box<dyn Error>> {
let project_id = self.get_project_id()?;
let client = Client::new();
let credentials_path = PathBuf::from(&self.credential_file);
let service_account = CustomServiceAccount::from_file(credentials_path)?;
let scopes = &["https://www.googleapis.com/auth/firebase.messaging"];
let token = service_account.token(scopes).await?;
let url = format!("https://fcm.googleapis.com/v1/projects/{project_id}/messages:send");
let payload = FcmPayload { message };
let response = client
.post(&url)
.bearer_auth(token.as_str())
.json(&payload)
.send()
.await?;
if response.status().is_success() {
response.text().await?;
Ok(())
} else {
let error_text = response.text().await?;
Err(format!("Failed to send notification: {error_text:#?}").into())
}
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::Write};
use tempfile;
use super::*;
fn setup_dummy_credentials(temp_dir: &tempfile::TempDir) -> String {
let credential_path = temp_dir.path().join("service-account.json");
let mut file = File::create(&credential_path).unwrap();
writeln!(
file,
r#"{{"project_id": "test-project", "client_email": "test@example.com"}}"#
)
.unwrap();
credential_path.to_str().unwrap().to_string()
}
#[test]
fn test_new_service() {
let service = FcmService::new("dummy.json");
assert_eq!(service.credential_file, "dummy.json");
}
#[test]
fn test_get_project_id_success() {
let temp_dir = tempfile::tempdir().unwrap();
let credential_file = setup_dummy_credentials(&temp_dir);
let service = FcmService::new(credential_file);
let project_id = service.get_project_id().unwrap();
assert_eq!(project_id, "test-project");
}
#[test]
fn test_get_project_id_missing_file() {
let service = FcmService::new("nonexistent.json");
let result = service.get_project_id();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().downcast_ref::<io::Error>(),
Some(err) if err.kind() == io::ErrorKind::NotFound
));
}
#[test]
fn test_get_project_id_invalid_json() {
let temp_dir = tempfile::tempdir().unwrap();
let credential_path = temp_dir.path().join("service-account.json");
let mut file = File::create(&credential_path).unwrap();
writeln!(file, "invalid json").unwrap();
let service = FcmService::new(credential_path.to_str().unwrap());
let result = service.get_project_id();
assert!(result.is_err());
}
}