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