bambulab_cloud/
types.rs

1use std::str::FromStr;
2
3use chrono::{DateTime, Utc};
4use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation};
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use url::Url;
8
9#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
10pub enum Region {
11    China,
12    Europe,
13    NorthAmerica,
14    AsiaPacific,
15    Other,
16}
17
18impl Region {
19    pub(crate) const fn is_china(self) -> bool {
20        matches!(self, Self::China)
21    }
22}
23
24#[derive(Debug, serde::Deserialize)]
25pub struct Device {
26    pub name: String,
27    pub online: bool,
28    pub dev_id: String,
29    pub print_status: String,
30    pub nozzle_diameter: f64,
31    pub dev_model_name: String,
32    pub dev_access_code: String,
33    pub dev_product_name: String,
34}
35
36impl Device {
37    /// Get the streaming URL for the camera on this device.
38    ///
39    /// # Errors
40    ///
41    /// This function can return a [`reqwest::Error`] if the request fails.
42    pub async fn get_bambu_camera_url(
43        &self,
44        client: &super::Client,
45    ) -> Result<Url, DeviceCameraError> {
46        let response = client
47            .client
48            .post(if client.region.is_china() {
49                "https://api.bambulab.cn/v1/iot-service/api/user/ttcode"
50            } else {
51                "https://api.bambulab.com/v1/iot-service/api/user/ttcode"
52            })
53            .header(
54                "Authorization",
55                &format!("Bearer {}", client.auth_token.jwt),
56            )
57            .header("user-id", client.auth_token.username.clone())
58            .json(&json!({ "dev_id": self.dev_id }))
59            .send()
60            .await?
61            .error_for_status()?
62            .json::<DeviceCameraResponse>()
63            .await?;
64
65        Ok(Url::from_str(&format!(
66            "bambu:///{}?authkey={}&passwd={}&region={}",
67            response.ttcode, response.authkey, response.passwd, response.region
68        ))?)
69    }
70}
71
72#[derive(Debug, serde::Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct Task {
75    pub id: u64,
76    pub design_id: u64,
77    pub design_title: String,
78    pub instance_id: u64,
79    pub model_id: String,
80    pub title: String,
81    pub cover: Url,
82    pub status: u64,
83    pub feedback_status: u64,
84    pub start_time: DateTime<Utc>,
85    pub end_time: DateTime<Utc>,
86    pub weight: f64,
87    pub length: u64,
88    pub cost_time: u64,
89    pub profile_id: u64,
90    pub plate_index: usize,
91    pub plate_name: String,
92    pub device_id: String,
93    pub ams_detail_mapping: Vec<AMSDetail>,
94    pub mode: String,
95    pub is_public_profile: bool,
96    pub is_printable: bool,
97    pub device_model: String,
98    pub device_name: String,
99    pub bed_type: String,
100}
101
102#[derive(Debug, serde::Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct AMSDetail {
105    #[serde(rename = "ams")]
106    pub position: usize,
107    pub source_color: String,
108    pub target_color: String,
109    pub filament_id: String,
110    pub filament_type: String,
111    pub target_filament_type: String,
112    pub weight: f64,
113}
114
115#[derive(Debug, serde::Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct Account {
118    pub uid: u64,
119    #[serde(rename = "account")]
120    pub email: String,
121    pub name: String,
122    pub avatar: Url,
123    pub fan_count: u64,
124    pub follow_count: u64,
125    pub like_count: u64,
126    pub collection_count: u64,
127    pub download_count: u64,
128    pub product_models: Vec<String>,
129    pub my_like_count: u64,
130    pub favourites_count: u64,
131    pub point: u64,
132    pub personal: Personal,
133}
134
135#[derive(Debug, serde::Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct Personal {
138    pub bio: String,
139    pub links: Vec<Url>,
140    pub task_weight_sum: f64,
141    pub task_length_sum: u64,
142    pub task_time_sum: u64,
143    pub background_url: Url,
144}
145
146#[derive(Debug)]
147pub struct Token {
148    pub username: String,
149    pub(crate) jwt: String,
150}
151
152#[derive(Debug, Deserialize)]
153struct JWTData {
154    username: String,
155}
156
157impl TryFrom<String> for Token {
158    type Error = jsonwebtoken::errors::Error;
159
160    fn try_from(jwt: String) -> Result<Self, Self::Error> {
161        let mut validation = Validation::new(Algorithm::RS256);
162        validation.insecure_disable_signature_validation();
163        validation.validate_aud = false;
164
165        let token: TokenData<JWTData> =
166            jsonwebtoken::decode(&jwt, &DecodingKey::from_secret(&[]), &validation)?;
167
168        Ok(Self {
169            jwt,
170            username: token.claims.username,
171        })
172    }
173}
174
175#[derive(serde::Deserialize)]
176pub struct LoginResponse {
177    #[serde(rename = "accessToken")]
178    pub(crate) access_token: String,
179}
180
181#[derive(serde::Deserialize)]
182pub struct DevicesResponse {
183    pub devices: Vec<Device>,
184}
185
186#[derive(Debug, serde::Deserialize)]
187pub struct TasksResponse {
188    pub total: usize,
189    pub hits: Vec<Task>,
190}
191
192#[derive(Debug, serde::Deserialize)]
193struct DeviceCameraResponse {
194    ttcode: String,
195    authkey: String,
196    passwd: String,
197    region: String,
198}
199
200#[derive(Debug, thiserror::Error)]
201pub enum DeviceCameraError {
202    #[error("failed to get camera URL")]
203    Reqwest(#[from] reqwest::Error),
204
205    #[error("failed to parse camera URL")]
206    Url(#[from] url::ParseError),
207}