1#![warn(missing_docs)]
2use 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
19pub 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#[derive(Clone)]
26pub struct FCMDeviceGroupClient {
27 url: Url,
28 client: HttpClient,
29 auth: Box<dyn GetToken + 'static>,
30}
31
32#[derive(Debug)]
34pub struct FCMDeviceGroup {
35 pub notification_key_name: String,
37 pub notification_key: String,
41}
42
43impl FCMDeviceGroupClient {
44 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 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 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 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 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 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 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 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}