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