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}