bambulab_cloud/
lib.rs

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    /// Create a new client by logging in with the provided credentials.
28    ///
29    /// # Errors
30    ///
31    /// This function can return a [`LoginError`] if the login request fails or the response cannot be decoded.
32    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    /// Get the account profile for the logged-in user.
56    ///
57    /// # Errors
58    ///
59    /// This function can return a [`reqwest::Error`] if the request fails.
60    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    /// Get a list of devices associated with the account.
76    ///
77    /// # Errors
78    ///
79    /// This function can return a [`reqwest::Error`] if the request fails.
80    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    /// Get a list of tasks associated with the account.
99    ///
100    /// # Errors
101    ///
102    /// This function can return a [`reqwest::Error`] if the request fails.
103    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    /// Get the MQTT host for the client's region.
129    #[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}