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 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={}®ion={}",
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}