1use anyhow::{Context, Result};
2use reqwest::{
3 header::{HeaderMap, HeaderValue},
4 Client,
5};
6use serde::Deserialize;
7use std::collections::HashMap;
8
9const API_BASE_URL: &str = "https://api.getbring.com/rest/v2/";
10const API_KEY: &str = "cof4Nc6D8saplXjE3h3HXqHH8m7VU2i1Gs0g85Sp";
11
12#[derive(Debug, Clone)]
13pub struct BringClient {
14 email: String,
15 password: String,
16 client: Client,
17 base_url: String,
18 uuid: Option<String>,
19 bearer_token: Option<String>,
20 refresh_token: Option<String>,
21 name: Option<String>,
22}
23
24#[derive(Debug, Deserialize)]
25pub struct AuthResponse {
26 pub name: String,
27 pub uuid: String,
28 pub access_token: String,
29 pub refresh_token: String,
30}
31
32#[derive(Debug, Deserialize)]
33pub struct GetItemsResponseEntry {
34 pub specification: String,
35 pub name: String,
36}
37
38#[derive(Debug, Deserialize)]
39pub struct GetItemsResponse {
40 pub uuid: String,
41 pub status: String,
42 pub purchase: Vec<GetItemsResponseEntry>,
43 pub recently: Vec<GetItemsResponseEntry>,
44}
45
46#[derive(Debug, Deserialize)]
47pub struct GetAllUsersFromListEntry {
48 #[serde(rename = "publicUuid")]
49 pub public_uuid: String,
50 pub name: String,
51 pub email: String,
52 #[serde(rename = "photoPath")]
53 pub photo_path: String,
54 #[serde(rename = "pushEnabled")]
55 pub push_enabled: bool,
56 #[serde(rename = "plusTryOut")]
57 pub plus_try_out: bool,
58 pub country: String,
59 pub language: String,
60}
61
62#[derive(Debug, Deserialize)]
63pub struct GetAllUsersFromListResponse {
64 pub users: Vec<GetAllUsersFromListEntry>,
65}
66
67#[derive(Debug, Deserialize)]
68pub struct LoadListsEntry {
69 #[serde(rename = "listUuid")]
70 pub list_uuid: String,
71 pub name: String,
72 pub theme: String,
73}
74
75#[derive(Debug, Deserialize)]
76pub struct LoadListsResponse {
77 pub lists: Vec<LoadListsEntry>,
78}
79
80#[derive(Debug, Deserialize)]
81pub struct GetItemsDetailsEntry {
82 pub uuid: String,
83 #[serde(rename = "itemId")]
84 pub item_id: String,
85 #[serde(rename = "listUuid")]
86 pub list_uuid: String,
87 #[serde(rename = "userIconItemId")]
88 pub user_icon_item_id: String,
89 #[serde(rename = "userSectionId")]
90 pub user_section_id: String,
91 #[serde(rename = "assignedTo")]
92 pub assigned_to: String,
93 #[serde(rename = "imageUrl")]
94 pub image_url: String,
95}
96
97impl BringClient {
98 pub fn new(email: String, password: String) -> Self {
99 Self {
100 email,
101 password,
102 client: Client::new(),
103 base_url: API_BASE_URL.to_string(),
104 uuid: None,
105 bearer_token: None,
106 refresh_token: None,
107 name: None,
108 }
109 }
110
111 fn get_headers(&self) -> HeaderMap {
112 let mut headers = HeaderMap::new();
113 headers.insert("X-BRING-API-KEY", HeaderValue::from_static(API_KEY));
114 headers.insert("X-BRING-CLIENT", HeaderValue::from_static("webApp"));
115 headers.insert("X-BRING-CLIENT-SOURCE", HeaderValue::from_static("webApp"));
116 headers.insert("X-BRING-COUNTRY", HeaderValue::from_static("DE"));
117
118 if let Some(uuid) = &self.uuid {
119 headers.insert("X-BRING-USER-UUID", HeaderValue::from_str(uuid).unwrap());
120 }
121
122 if let Some(token) = &self.bearer_token {
123 headers.insert(
124 "Authorization",
125 HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
126 );
127 }
128
129 headers
130 }
131
132 pub async fn login(&mut self) -> Result<()> {
133 let params = [
134 ("email", self.email.as_str()),
135 ("password", self.password.as_str()),
136 ];
137
138 let response = self
139 .client
140 .post(format!("{}bringauth", self.base_url))
141 .form(¶ms)
142 .send()
143 .await
144 .context("Failed to send login request")?;
145
146 let auth_response: AuthResponse = response
147 .json()
148 .await
149 .context("Failed to parse login response")?;
150
151 self.name = Some(auth_response.name);
152 self.uuid = Some(auth_response.uuid);
153 self.bearer_token = Some(auth_response.access_token);
154 self.refresh_token = Some(auth_response.refresh_token);
155
156 Ok(())
157 }
158
159 pub async fn load_lists(&self) -> Result<LoadListsResponse> {
160 let uuid = self.uuid.as_ref().context("Not logged in")?;
161
162 let response = self
163 .client
164 .get(format!("{}bringusers/{}/lists", self.base_url, uuid))
165 .headers(self.get_headers())
166 .send()
167 .await
168 .context("Failed to load lists")?;
169
170 response
171 .json()
172 .await
173 .context("Failed to parse lists response")
174 }
175
176 pub async fn get_items(&self, list_uuid: &str) -> Result<GetItemsResponse> {
177 let response = self
178 .client
179 .get(format!("{}bringlists/{}", self.base_url, list_uuid))
180 .headers(self.get_headers())
181 .send()
182 .await
183 .context("Failed to get items")?;
184
185 response
186 .json()
187 .await
188 .context("Failed to parse items response")
189 }
190
191 pub async fn get_items_details(&self, list_uuid: &str) -> Result<Vec<GetItemsDetailsEntry>> {
192 let response = self
193 .client
194 .get(format!("{}bringlists/{}/details", self.base_url, list_uuid))
195 .headers(self.get_headers())
196 .send()
197 .await
198 .context("Failed to get item details")?;
199
200 response
201 .json()
202 .await
203 .context("Failed to parse item details response")
204 }
205
206 pub async fn save_item(
207 &self,
208 list_uuid: &str,
209 item_name: &str,
210 specification: &str,
211 ) -> Result<()> {
212 let mut headers = self.get_headers();
213 headers.insert(
214 "Content-Type",
215 HeaderValue::from_static("application/x-www-form-urlencoded; charset=UTF-8"),
216 );
217
218 let params = [
219 ("purchase", item_name),
220 ("recently", ""),
221 ("specification", specification),
222 ("remove", ""),
223 ("sender", "null"),
224 ];
225
226 self.client
227 .put(format!("{}bringlists/{}", self.base_url, list_uuid))
228 .headers(headers)
229 .form(¶ms)
230 .send()
231 .await
232 .context("Failed to save item")?;
233
234 Ok(())
235 }
236
237 pub async fn remove_item(&self, list_uuid: &str, item_name: &str) -> Result<()> {
238 let mut headers = self.get_headers();
239 headers.insert(
240 "Content-Type",
241 HeaderValue::from_static("application/x-www-form-urlencoded; charset=UTF-8"),
242 );
243
244 let params = [
245 ("purchase", ""),
246 ("recently", ""),
247 ("specification", ""),
248 ("remove", item_name),
249 ("sender", "null"),
250 ];
251
252 self.client
253 .put(format!("{}bringlists/{}", self.base_url, list_uuid))
254 .headers(headers)
255 .form(¶ms)
256 .send()
257 .await
258 .context("Failed to remove item")?;
259
260 Ok(())
261 }
262
263 pub async fn get_all_users_from_list(
264 &self,
265 list_uuid: &str,
266 ) -> Result<GetAllUsersFromListResponse> {
267 let response = self
268 .client
269 .get(format!("{}bringlists/{}/users", self.base_url, list_uuid))
270 .headers(self.get_headers())
271 .send()
272 .await
273 .context("Failed to get users from list")?;
274
275 response
276 .json()
277 .await
278 .context("Failed to parse users response")
279 }
280
281 pub async fn load_translations(&self, locale: &str) -> Result<HashMap<String, String>> {
282 let response = self
283 .client
284 .get(format!(
285 "https://web.getbring.com/locale/articles.{}.json",
286 locale
287 ))
288 .send()
289 .await
290 .context("Failed to load translations")?;
291
292 response
293 .json()
294 .await
295 .context("Failed to parse translations")
296 }
297}