use anyhow::{Context, Result};
use reqwest::{
header::{HeaderMap, HeaderValue},
Client,
};
use serde::Deserialize;
use std::collections::HashMap;
use tracing::instrument;
const API_BASE_URL: &str = "https://api.getbring.com/rest/v2/";
const API_KEY: &str = "cof4Nc6D8saplXjE3h3HXqHH8m7VU2i1Gs0g85Sp";
#[derive(Debug, Clone)]
pub struct BringClient {
email: String,
password: String,
client: Client,
base_url: String,
uuid: Option<String>,
bearer_token: Option<String>,
refresh_token: Option<String>,
name: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct AuthResponse {
pub name: String,
pub uuid: String,
pub access_token: String,
pub refresh_token: String,
}
#[derive(Debug, Deserialize)]
pub struct GetItemsResponseEntry {
pub specification: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
pub struct GetItemsResponse {
pub uuid: String,
pub status: String,
pub purchase: Vec<GetItemsResponseEntry>,
pub recently: Vec<GetItemsResponseEntry>,
}
#[derive(Debug, Deserialize)]
pub struct GetAllUsersFromListEntry {
#[serde(rename = "publicUuid")]
pub public_uuid: String,
pub name: String,
pub email: String,
#[serde(rename = "photoPath")]
pub photo_path: String,
#[serde(rename = "pushEnabled")]
pub push_enabled: bool,
#[serde(rename = "plusTryOut")]
pub plus_try_out: bool,
pub country: String,
pub language: String,
}
#[derive(Debug, Deserialize)]
pub struct GetAllUsersFromListResponse {
pub users: Vec<GetAllUsersFromListEntry>,
}
#[derive(Debug, Deserialize)]
pub struct LoadListsEntry {
#[serde(rename = "listUuid")]
pub list_uuid: String,
pub name: String,
pub theme: String,
}
#[derive(Debug, Deserialize)]
pub struct LoadListsResponse {
pub lists: Vec<LoadListsEntry>,
}
#[derive(Debug, Deserialize)]
pub struct GetItemsDetailsEntry {
pub uuid: String,
#[serde(rename = "itemId")]
pub item_id: String,
#[serde(rename = "listUuid")]
pub list_uuid: String,
#[serde(rename = "userIconItemId")]
pub user_icon_item_id: String,
#[serde(rename = "userSectionId")]
pub user_section_id: String,
#[serde(rename = "assignedTo")]
pub assigned_to: String,
#[serde(rename = "imageUrl")]
pub image_url: String,
}
impl BringClient {
pub fn new(email: String, password: String) -> Self {
Self {
email,
password,
client: Client::new(),
base_url: API_BASE_URL.to_string(),
uuid: None,
bearer_token: None,
refresh_token: None,
name: None,
}
}
#[instrument(skip(self))]
fn get_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("X-BRING-API-KEY", HeaderValue::from_static(API_KEY));
headers.insert("X-BRING-CLIENT", HeaderValue::from_static("webApp"));
headers.insert("X-BRING-CLIENT-SOURCE", HeaderValue::from_static("webApp"));
headers.insert("X-BRING-COUNTRY", HeaderValue::from_static("DE"));
if let Some(uuid) = &self.uuid {
headers.insert("X-BRING-USER-UUID", HeaderValue::from_str(uuid).unwrap());
}
if let Some(token) = &self.bearer_token {
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
);
}
headers
}
#[instrument(skip(self))]
pub async fn login(&mut self) -> Result<()> {
let params = [
("email", self.email.as_str()),
("password", self.password.as_str()),
];
let response = self
.client
.post(format!("{}bringauth", self.base_url))
.form(¶ms)
.send()
.await
.context("Failed to send login request")?;
let auth_response: AuthResponse = response
.json()
.await
.context("Failed to parse login response")?;
self.name = Some(auth_response.name);
self.uuid = Some(auth_response.uuid);
self.bearer_token = Some(auth_response.access_token);
self.refresh_token = Some(auth_response.refresh_token);
Ok(())
}
#[instrument(skip(self))]
pub async fn load_lists(&self) -> Result<LoadListsResponse> {
let uuid = self.uuid.as_ref().context("Not logged in")?;
let response = self
.client
.get(format!("{}bringusers/{}/lists", self.base_url, uuid))
.headers(self.get_headers())
.send()
.await
.context("Failed to load lists")?;
response
.json()
.await
.context("Failed to parse lists response")
}
#[instrument(skip(self))]
pub async fn get_items(&self, list_uuid: &str) -> Result<GetItemsResponse> {
let response = self
.client
.get(format!("{}bringlists/{}", self.base_url, list_uuid))
.headers(self.get_headers())
.send()
.await
.context("Failed to get items")?;
response
.json()
.await
.context("Failed to parse items response")
}
#[instrument(skip(self))]
pub async fn get_items_details(&self, list_uuid: &str) -> Result<Vec<GetItemsDetailsEntry>> {
let response = self
.client
.get(format!("{}bringlists/{}/details", self.base_url, list_uuid))
.headers(self.get_headers())
.send()
.await
.context("Failed to get item details")?;
response
.json()
.await
.context("Failed to parse item details response")
}
#[instrument(skip(self))]
pub async fn save_item(
&self,
list_uuid: &str,
item_name: &str,
specification: &str,
) -> Result<()> {
let mut headers = self.get_headers();
headers.insert(
"Content-Type",
HeaderValue::from_static("application/x-www-form-urlencoded; charset=UTF-8"),
);
let params = [
("purchase", item_name),
("recently", ""),
("specification", specification),
("remove", ""),
("sender", "null"),
];
self.client
.put(format!("{}bringlists/{}", self.base_url, list_uuid))
.headers(headers)
.form(¶ms)
.send()
.await
.context("Failed to save item")?;
Ok(())
}
#[instrument(skip(self))]
pub async fn remove_item(&self, list_uuid: &str, item_name: &str) -> Result<()> {
let mut headers = self.get_headers();
headers.insert(
"Content-Type",
HeaderValue::from_static("application/x-www-form-urlencoded; charset=UTF-8"),
);
let params = [
("purchase", ""),
("recently", ""),
("specification", ""),
("remove", item_name),
("sender", "null"),
];
self.client
.put(format!("{}bringlists/{}", self.base_url, list_uuid))
.headers(headers)
.form(¶ms)
.send()
.await
.context("Failed to remove item")?;
Ok(())
}
#[instrument(skip(self))]
pub async fn get_all_users_from_list(
&self,
list_uuid: &str,
) -> Result<GetAllUsersFromListResponse> {
let response = self
.client
.get(format!("{}bringlists/{}/users", self.base_url, list_uuid))
.headers(self.get_headers())
.send()
.await
.context("Failed to get users from list")?;
response
.json()
.await
.context("Failed to parse users response")
}
#[instrument(skip(self))]
pub async fn load_translations(&self, locale: &str) -> Result<HashMap<String, String>> {
let response = self
.client
.get(format!(
"https://web.getbring.com/locale/articles.{}.json",
locale
))
.send()
.await
.context("Failed to load translations")?;
response
.json()
.await
.context("Failed to parse translations")
}
#[instrument(skip(self))]
pub async fn get_list_id_by_name(&self, list_name: &str) -> Result<Option<String>> {
let lists = self.load_lists().await?;
Ok(lists
.lists
.into_iter()
.find(|list| list.name.to_lowercase() == list_name.to_lowercase())
.map(|list| list.list_uuid))
}
#[instrument(skip(self))]
pub async fn get_list_id_by_name_required(&self, list_name: &str) -> Result<String> {
self.get_list_id_by_name(list_name)
.await?
.context(format!("List with name '{}' not found", list_name))
}
}