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