Skip to main content

fcm_device_group/
lib.rs

1#![warn(missing_docs)]
2//! A crate for using Firebase Cloud Messaging device groups.
3//! See <https://firebase.google.com/docs/cloud-messaging/android/topic-messaging>
4//!
5//! Note that you will have to manually depend on a `reqwest` TLS feature if the default-tls feature is disabled.
6pub use google_apis_common::{
7    GetToken,
8    auth::{GetTokenClone, NoToken},
9};
10use reqwest::{
11    Client as HttpClient, IntoUrl, RequestBuilder, Response, Url,
12    header::{self, HeaderMap, HeaderValue},
13};
14
15pub use raw::{Operation, OperationResponse};
16
17use error::operation_errors::OperationResult;
18
19pub mod error;
20mod raw;
21
22/// Default URL used for FCM device groups
23pub const FIREBASE_NOTIFICATION_URL: &str = "https://fcm.googleapis.com/fcm/notification";
24
25const FCM_DEVICE_GROUP_SCOPES: &[&str] = &["https://www.googleapis.com/auth/firebase.messaging"];
26
27/// Client to use fcm device groups
28#[derive(Clone)]
29pub struct FCMDeviceGroupClient {
30    url: Url,
31    client: HttpClient,
32    auth: Box<dyn GetToken + 'static>,
33}
34
35/// A Representation of an FCM Device group
36#[derive(Debug)]
37pub struct FCMDeviceGroup {
38    /// Name of the device group
39    pub notification_key_name: String,
40    /// Key for this device group.
41    ///
42    /// Note that one device group may have multiple keys
43    pub notification_key: String,
44}
45
46impl FCMDeviceGroupClient {
47    /// Creates a new `FCMDeviceGroupClient` with the default url and the provided bearer auth string
48    pub fn new(
49        sender_id: &str,
50        auth: impl GetToken + 'static,
51    ) -> Result<Self, error::FCMDeviceGroupClientCreationError> {
52        Self::with_url(FIREBASE_NOTIFICATION_URL, sender_id, auth)
53    }
54
55    /// Creates a new `FCMDeviceGroupClient` with the given url and the provided bearer auth string
56    pub fn with_url(
57        url: impl IntoUrl,
58        sender_id: &str,
59        auth: impl GetToken + 'static,
60    ) -> Result<Self, error::FCMDeviceGroupClientCreationError> {
61        let mut headers = HeaderMap::new();
62        headers.insert("project_id", header::HeaderValue::try_from(sender_id)?);
63        headers.insert(
64            "access_token_auth",
65            header::HeaderValue::from_static("true"),
66        );
67
68        Ok(Self {
69            url: url.into_url().unwrap(),
70            client: HttpClient::builder()
71                .default_headers(headers)
72                .connection_verbose(true)
73                .build()?,
74            auth: Box::new(auth),
75        })
76    }
77
78    /// Creates a new `FCMDeviceGroupClient` with the given url and client. Note that the creator of the client
79    /// is responsible for adding authorization headers
80    pub fn with_client(
81        client: HttpClient,
82        url: impl IntoUrl,
83        auth: impl GetToken + 'static,
84    ) -> Self {
85        Self {
86            url: url.into_url().unwrap(),
87            client,
88            auth: Box::new(auth),
89        }
90    }
91
92    /// Apply the given operation with with the client.
93    pub async fn apply(
94        &self,
95        operation: Operation,
96    ) -> Result<
97        OperationResponse,
98        error::FCMDeviceGroupsRequestError<error::FCMDeviceGroupsBadRequest>,
99    > {
100        let response = self.apply_raw(operation).await?;
101        error::FCMDeviceGroupsRequestError::json_response(response).await
102    }
103
104    /// Create a new group with the provided name and ID
105    pub async fn create_group(
106        &self,
107        notification_key_name: String,
108        registration_ids: Vec<String>,
109    ) -> OperationResult<FCMDeviceGroup, error::operation_errors::CreateGroupError> {
110        self.apply_operation(Operation::Create {
111            notification_key_name: notification_key_name.clone(),
112            registration_ids,
113        })
114        .await
115    }
116
117    /// Add a set of registration IDS to the group
118    pub async fn add_to_group(
119        &self,
120        group: FCMDeviceGroup,
121        registration_ids: Vec<String>,
122    ) -> OperationResult<FCMDeviceGroup, error::operation_errors::ChangeGroupMembersError> {
123        self.apply_operation(Operation::Add {
124            notification_key_name: Some(group.notification_key_name),
125            notification_key: group.notification_key,
126            registration_ids,
127        })
128        .await
129    }
130
131    /// Remove a set of registration IDS to the group
132    pub async fn remove_from_group(
133        &self,
134        group: FCMDeviceGroup,
135        registration_ids: Vec<String>,
136    ) -> OperationResult<FCMDeviceGroup, error::operation_errors::ChangeGroupMembersError> {
137        self.apply_operation(Operation::Remove {
138            notification_key_name: Some(group.notification_key_name),
139            notification_key: group.notification_key,
140            registration_ids,
141        })
142        .await
143    }
144
145    /// Use this client to request the notification key for a given name
146    pub async fn get_key(
147        &self,
148        notification_key_name: String,
149    ) -> OperationResult<FCMDeviceGroup, error::operation_errors::GetKeyError> {
150        let request = self
151            .client
152            .get(self.url.clone())
153            .query(&[("notification_key_name", notification_key_name.as_str())])
154            .header(
155                header::CONTENT_TYPE,
156                HeaderValue::from_static("application/json"),
157            );
158        let response = self
159            .add_token(request)
160            .await
161            .map_err(error::RawError::GetTokenError)?
162            .send()
163            .await?;
164        let response =
165            error::FCMDeviceGroupsRequestError::<error::operation_errors::GetKeyError>::json_response::<OperationResponse>(response)
166                .await?;
167        Ok(FCMDeviceGroup {
168            notification_key_name,
169            notification_key: response.notification_key,
170        })
171    }
172
173    async fn apply_raw(&self, operation: Operation) -> Result<Response, error::RawError> {
174        let request = self.client.post(self.url.clone()).json(&operation);
175
176        let request = self
177            .add_token(request)
178            .await
179            .map_err(error::RawError::GetTokenError)?;
180
181        Ok(request.send().await?)
182    }
183
184    async fn add_token(
185        &self,
186        request: RequestBuilder,
187    ) -> Result<RequestBuilder, Box<dyn std::error::Error + Send + Sync>> {
188        match self.auth.get_token(FCM_DEVICE_GROUP_SCOPES).await? {
189            Some(token) => Ok(request.bearer_auth(token)),
190            None => Ok(request),
191        }
192    }
193
194    async fn apply_operation<E: error::FCMDeviceGroupError>(
195        &self,
196        operation: Operation,
197    ) -> OperationResult<FCMDeviceGroup, E> {
198        let key_name = match &operation {
199            Operation::Create {
200                notification_key_name,
201                ..
202            } => notification_key_name.to_owned(),
203            Operation::Add {
204                notification_key_name,
205                ..
206            } => notification_key_name
207                .as_ref()
208                .expect("Applying an operation should always have a key name")
209                .to_owned(),
210            Operation::Remove {
211                notification_key_name,
212                ..
213            } => notification_key_name
214                .as_ref()
215                .expect("Applying an operation should always have a key name")
216                .to_owned(),
217        };
218        let response = self.apply_raw(operation).await?;
219        let response =
220            error::FCMDeviceGroupsRequestError::<E>::json_response::<OperationResponse>(response)
221                .await?;
222        Ok(FCMDeviceGroup {
223            notification_key_name: key_name,
224            notification_key: response.notification_key,
225        })
226    }
227}