use serde::Serialize;
use serde_json::json;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing::instrument;
use crate::error::NetworkError;
use crate::error::ResultMapError;
use crate::FcmError;
use crate::SharedTokenManager;
pub struct FcmNotification {
pub title: String,
pub body: String,
}
#[instrument(level = "info", skip(data_payload, notification, token_manager))]
pub async fn send_fcm_message<T: Serialize>(
device_token: &str,
notification: Option<FcmNotification>,
data_payload: Option<T>,
token_manager: &SharedTokenManager,
project_id: &str,
) -> Result<(), FcmError> {
info!("Sending FCM message to device: {}", device_token);
let url = format!("https://fcm.googleapis.com/v1/projects/{project_id}/messages:send");
send_fcm_message_with_url(
device_token,
notification,
data_payload,
token_manager,
&url,
)
.await
}
#[instrument(level = "debug", skip(data_payload, notification, token_manager))]
pub async fn send_fcm_message_with_url<T: Serialize>(
device_token: &str,
notification: Option<FcmNotification>,
data_payload: Option<T>,
token_manager: &SharedTokenManager,
fcm_url: &str,
) -> Result<(), FcmError> {
let access_token = {
let mut token_manager_guard = token_manager.lock().await;
token_manager_guard.get_token().await?
};
let client = reqwest::Client::new();
let payload = create_payload(device_token, notification, data_payload)?;
debug!("Requesting access token");
let res = client
.post(fcm_url)
.bearer_auth(access_token)
.json(&payload)
.send()
.await
.map_err(NetworkError::SendRequestError)
.map_fcm_err()?;
if res.status().is_success() {
debug!("FCM message sent successfully");
Ok(())
} else {
let status = res.status().as_u16();
let text = res
.text()
.await
.map_err(NetworkError::ResponseError)
.map_fcm_err()?;
error!(
"FCM message send successfully, but server returned an error. Status: {}, Response: {}",
status, text
);
Err(NetworkError::ServerError(status, Some(text))).map_fcm_err()
}
}
fn create_payload<T: Serialize>(
device_token: &str,
notification: Option<FcmNotification>,
data_payload: Option<T>,
) -> Result<serde_json::Value, FcmError> {
let payload = match (notification, data_payload) {
(Some(notification), Some(data_payload)) => {
let data = serde_json::to_value(data_payload).map_err(FcmError::SerializationError)?;
json!({
"message": {
"token": device_token,
"notification": {
"title": notification.title,
"body": notification.body
},
"data": data
}
})
}
(None, Some(data_payload)) => {
let data = serde_json::to_value(data_payload).map_err(FcmError::SerializationError)?;
json!({
"message": {
"token": device_token,
"data": data
}
})
}
(Some(notification), None) => json!({
"message": {
"token": device_token,
"notification": {
"title": notification.title,
"body": notification.body
}
}
}),
_ => return Err(FcmError::FcmInvalidPayloadError),
};
Ok(payload)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_payload_with_notification_and_data() {
let device_token = "test_device_token";
let notification = Some(FcmNotification {
title: "Test Title".to_string(),
body: "Test Body".to_string(),
});
let data_payload = Some(json!({
"key": "value"
}));
let payload = create_payload(device_token, notification, data_payload).unwrap();
assert_eq!(payload["message"]["token"], device_token);
assert_eq!(payload["message"]["notification"]["title"], "Test Title");
assert_eq!(payload["message"]["notification"]["body"], "Test Body");
assert_eq!(payload["message"]["data"]["key"], "value");
}
#[tokio::test]
async fn test_create_payload_with_only_notification() {
let device_token = "test_device_token";
let notification = Some(FcmNotification {
title: "Test Title".to_string(),
body: "Test Body".to_string(),
});
let data_payload: Option<serde_json::Value> = None;
let payload = create_payload(device_token, notification, data_payload).unwrap();
assert_eq!(payload["message"]["token"], device_token);
assert_eq!(payload["message"]["notification"]["title"], "Test Title");
assert_eq!(payload["message"]["notification"]["body"], "Test Body");
assert!(payload["message"]["data"].is_null());
}
#[tokio::test]
async fn test_create_payload_with_only_data() {
let device_token = "test_device_token";
let notification: Option<FcmNotification> = None;
let data_payload = Some(json!({
"key": "value"
}));
let payload = create_payload(device_token, notification, data_payload).unwrap();
assert_eq!(payload["message"]["token"], device_token);
assert!(payload["message"]["notification"].is_null());
assert_eq!(payload["message"]["data"]["key"], "value");
}
#[derive(serde::Serialize)]
struct TestData {
key1: String,
key2: String,
}
#[tokio::test]
async fn test_create_payload_with_only_struct_data() {
let device_token = "test_device_token";
let notification: Option<FcmNotification> = None;
let data_payload = TestData {
key1: "value1".to_string(),
key2: "value2".to_string(),
};
let payload = create_payload(device_token, notification, Some(data_payload)).unwrap();
assert_eq!(payload["message"]["token"], device_token);
assert!(payload["message"]["notification"].is_null());
assert_eq!(payload["message"]["data"]["key1"], "value1");
assert_eq!(payload["message"]["data"]["key2"], "value2");
}
#[tokio::test]
async fn test_create_payload_with_no_notification_and_no_data() {
let device_token = "test_device_token";
let notification: Option<FcmNotification> = None;
let data_payload: Option<serde_json::Value> = None;
let payload = create_payload(device_token, notification, data_payload);
assert!(payload.is_err());
}
}