1#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
2
3mod types;
4
5use serde_json::json;
6
7pub use types::{Account, Device, Region, Task};
8use types::{DevicesResponse, LoginResponse, TasksResponse, Token};
9
10#[derive(Debug)]
11pub struct Client {
12 region: Region,
13 client: reqwest::Client,
14 pub(crate) auth_token: Token,
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum LoginError {
19 #[error("failed to send login request")]
20 Reqwest(#[from] reqwest::Error),
21
22 #[error("failed to parse login response")]
23 Decode(#[from] jsonwebtoken::errors::Error),
24}
25
26impl Client {
27 pub async fn login(region: Region, email: &str, password: &str) -> Result<Self, LoginError> {
33 let client = reqwest::Client::new();
34
35 let response = client
36 .post(if region.is_china() {
37 "https://api.bambulab.cn/v1/user-service/user/login"
38 } else {
39 "https://api.bambulab.com/v1/user-service/user/login"
40 })
41 .json(&json!({ "account": email, "password": password }))
42 .send()
43 .await?
44 .error_for_status()?
45 .json::<LoginResponse>()
46 .await?;
47
48 Ok(Self {
49 region,
50 client,
51 auth_token: Token::try_from(response.access_token)?,
52 })
53 }
54
55 pub async fn get_profile(&self) -> Result<Account, reqwest::Error> {
61 self.client
62 .get(if self.region.is_china() {
63 "https://api.bambulab.cn/v1/user-service/my/profile"
64 } else {
65 "https://api.bambulab.com/v1/user-service/my/profile"
66 })
67 .header("Authorization", format!("Bearer {}", self.auth_token.jwt))
68 .send()
69 .await?
70 .error_for_status()?
71 .json()
72 .await
73 }
74
75 pub async fn get_devices(&self) -> Result<Vec<Device>, reqwest::Error> {
81 let response = self
82 .client
83 .get(if self.region.is_china() {
84 "https://api.bambulab.cn/v1/iot-service/api/user/bind"
85 } else {
86 "https://api.bambulab.com/v1/iot-service/api/user/bind"
87 })
88 .header("Authorization", format!("Bearer {}", self.auth_token.jwt))
89 .send()
90 .await?
91 .error_for_status()?
92 .json::<DevicesResponse>()
93 .await?;
94
95 Ok(response.devices)
96 }
97
98 pub async fn get_tasks(
104 &self,
105 only_device: Option<String>,
106 ) -> Result<Vec<Task>, reqwest::Error> {
107 let response = self
108 .client
109 .get(if self.region.is_china() {
110 "https://api.bambulab.cn/v1/user-service/my/tasks"
111 } else {
112 "https://api.bambulab.com/v1/user-service/my/tasks"
113 })
114 .query(&[
115 ("limit", "500".to_string()),
116 ("deviceId", only_device.unwrap_or_default()),
117 ])
118 .header("Authorization", format!("Bearer {}", self.auth_token.jwt))
119 .send()
120 .await?
121 .error_for_status()?
122 .json::<TasksResponse>()
123 .await?;
124
125 Ok(response.hits)
126 }
127
128 #[must_use]
130 pub const fn mqtt_host(&self) -> &str {
131 if self.region.is_china() {
132 "cn.mqtt.bambulab.com"
133 } else {
134 "us.mqtt.bambulab.com"
135 }
136 }
137}