fcm_rs/
client.rs

1//! Provides the `FcmClient` struct for interacting with the FCM API.
2use crate::error::FcmError;
3use crate::models::{FcmSendRequest, FcmSendResult, FcmSuccessResponse, Message};
4use reqwest::Client;
5use yup_oauth2::authenticator::Authenticator;
6use yup_oauth2::hyper::client::HttpConnector;
7use yup_oauth2::hyper_rustls::HttpsConnector;
8use yup_oauth2::{read_service_account_key, ServiceAccountAuthenticator};
9
10/// Firebase Cloud Messaging (FCM) client.
11pub struct FcmClient {
12    /// Authenticator for OAuth2.
13    auth: Authenticator<HttpsConnector<HttpConnector>>,
14    /// HTTP client for making requests.
15    http_client: Client,
16    /// Firebase project ID.
17    project_id: String,
18}
19
20impl FcmClient {
21    /// Creates a new `FcmClient` instance.
22    ///
23    /// # Arguments
24    ///
25    /// * `service_account_key_path` - Path to the service account key JSON file.
26    ///
27    /// # Errors
28    ///
29    /// Returns an `FcmError` if:
30    ///
31    /// * The service account key cannot be read.
32    /// * The authenticator cannot be built.
33    /// * Any other error occurs during initialization.
34    pub async fn new(service_account_key_path: &str) -> Result<Self, FcmError> {
35        let secret = read_service_account_key(service_account_key_path).await?;
36        let project_id = match secret.project_id {
37            Some(ref id) => id.clone(),
38            None => {
39                return Err(FcmError::AuthError(
40                    "Service account key JSON file missing project ID".to_string(),
41                ));
42            }
43        };
44        let auth = ServiceAccountAuthenticator::builder(secret).build().await?;
45        Ok(Self {
46            auth,
47            http_client: Client::new(),
48            project_id,
49        })
50    }
51
52    /// Sends an FCM message.
53    ///
54    /// This method constructs and sends an HTTP request to the FCM API to deliver a
55    /// message to the specified recipients. It handles authentication, constructs the
56    /// necessary request, and processes the response from the FCM service.
57    ///
58    /// # Arguments
59    ///
60    /// * `message` - The `Message` to send.
61    ///
62    /// # Errors
63    ///
64    /// Returns an `FcmError` if there's an issue with the request, authentication,
65    /// JSON (de)serialization, the response, or any other error during the sending process.
66    pub async fn send(&self, message: Message) -> Result<FcmSuccessResponse, FcmError> {
67        let url = self.build_url();
68        let token_str = self.get_token().await?;
69        let request = FcmSendRequest { message };
70
71        let response = self
72            .http_client
73            .post(url)
74            .header("Authorization", format!("Bearer {:?}", token_str))
75            .json(&request) // Send the request object
76            .send()
77            .await?;
78
79        response
80            .json::<FcmSendResult>()
81            .await
82            .map_err(FcmError::from)?
83            .into()
84    }
85
86    /// Constructs the URL for sending FCM messages.
87    ///
88    /// # Returns
89    ///
90    /// Returns a `String` containing the URL for the FCM API endpoint.
91    fn build_url(&self) -> String {
92        format!(
93            "https://fcm.googleapis.com/v1/projects/{}/messages:send",
94            self.project_id
95        )
96    }
97
98    /// Retrieves an OAuth2 token for the FCM API.
99    ///
100    /// This method requests an OAuth2 token from the authenticator that is required to authenticate
101    /// requests to the FCM API.
102    ///
103    /// # Errors
104    ///
105    /// Returns an `FcmError` if:
106    ///
107    /// * The token cannot be retrieved from the authenticator.
108    /// * Any other error occurs while fetching the token.
109    ///
110    /// # Returns
111    ///
112    /// On success, returns the OAuth2 token as a `String`. If the token cannot be retrieved,
113    /// an `FcmError` is returned.
114    async fn get_token(&self) -> Result<String, FcmError> {
115        let token = self
116            .auth
117            .token(&["https://www.googleapis.com/auth/firebase.messaging"])
118            .await?;
119
120        token.token().map(|s| s.to_string()).ok_or_else(|| {
121            FcmError::AuthError("Failed to retrieve token from authenticator".to_string())
122        })
123    }
124}