//! # Firebase notifications helper //! //! Helper used to send notifications through the Firebase Cloud Messaging services //! //! @author Pierre Hubert use std::collections::HashMap; use reqwest::blocking::Client; use reqwest::header::{HeaderMap, HeaderValue}; use serde::Serialize; use crate::constants::FIREBASE_PUSH_MESSAGE_URL; use crate::data::api_client::APIClient; use crate::data::error::{ExecError, Res}; use crate::data::push_notification::PushNotification; use crate::data::user_token::{PushNotificationToken, UserAccessToken}; use crate::helpers::api_helper; struct FirebaseClientIdentifier { project_name: String, authorization_token: String, } #[derive(Serialize)] struct FirebaseNotification { title: String, body: String, image: Option, } #[derive(Serialize)] struct FirebaseAndroidNotification { tag: String, } #[derive(Serialize)] struct FirebaseAndroid { notification: FirebaseAndroidNotification, ttl: Option, } #[derive(Serialize)] struct FirebaseMessage { token: String, notification: FirebaseNotification, android: FirebaseAndroid, } #[derive(Serialize)] struct FirebaseNotificationRequest { validate_only: bool, message: FirebaseMessage, } /// Get short-lived authorization token for a specific client fn get_credentials(client: &APIClient) -> Res { let service_account_file = match &client.firebase_service_account_file { Some(file) => file, None => { return Err(ExecError::boxed_string(format!("No service account file for client {}!", client.name))); } }; let token = gouth::Builder::new() .scopes(&["https://www.googleapis.com/auth/firebase.messaging"]) .json(service_account_file) .build()? .header_value()? .to_string(); Ok(token) } /// Send a single notification through Firebase service fn send_notification(n: &PushNotification, client_token: &str, access: &FirebaseClientIdentifier) -> Res { let notif = FirebaseNotificationRequest { validate_only: false, message: FirebaseMessage { token: client_token.to_string(), notification: FirebaseNotification { title: n.title.to_string(), body: n.body.to_string(), image: n.image.clone(), }, android: FirebaseAndroid { notification: FirebaseAndroidNotification { tag: n.id.to_string(), }, ttl: n.timeout.map(|t| format!("{}s", t)), }, }, }; let mut headers = HeaderMap::new(); headers.insert("Authorization", HeaderValue::from_str(&access.authorization_token)?); let client = Client::builder().default_headers(headers).build()?; client .post(&FIREBASE_PUSH_MESSAGE_URL.replace("{PROJECT_ID}", &access.project_name)) .json(¬if) .send()?; Ok(()) } /// Send a notification pub fn push_notifications(n: &PushNotification, targets: Vec) -> Res { let mut tokens_cache: HashMap = HashMap::new(); for target in targets { // Get an access token if required if !tokens_cache.contains_key(&target.client_id) { let client = api_helper::get_by_id(target.client_id)?; tokens_cache.insert(target.client_id, FirebaseClientIdentifier { authorization_token: get_credentials(&client)?, project_name: client.firebase_project_name .ok_or(ExecError::boxed_new("Missing firebase project name!"))?, }); } let authorization_token = match tokens_cache.get(&target.client_id) { None => { return Err(ExecError::boxed_new("Should have a Firebase token now!!!")); } Some(token) => token }; let client_token = match &target.push_notifications_token { PushNotificationToken::FIREBASE(token) => token, _ => { return Err(ExecError::boxed_new("Invalid token!")); } }; if let Err(e) = send_notification(n, client_token, authorization_token) { eprintln!("Failed to send a push notification to a device! Error: {}", e); } } Ok(()) }