1use std::collections::HashMap;
2
3use reqwest::Url;
4use serde::{Deserialize, Serialize};
5use serde::de::{self, Visitor};
6use std::fmt;
7
8use crate::CyberdropError;
9
10#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
15#[serde(transparent)]
16pub struct AuthToken {
17 pub(crate) token: String,
18}
19
20impl AuthToken {
21 pub fn new(token: impl Into<String>) -> Self {
23 Self {
24 token: token.into(),
25 }
26 }
27
28 pub fn as_str(&self) -> &str {
30 &self.token
31 }
32
33 pub fn into_string(self) -> String {
35 self.token
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
41pub struct Permissions {
42 pub user: bool,
44 pub poweruser: bool,
46 pub moderator: bool,
48 pub admin: bool,
50 pub superadmin: bool,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct TokenVerification {
57 pub success: bool,
59 pub username: String,
61 pub permissions: Permissions,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct Album {
72 pub id: u64,
74 pub name: String,
76 #[serde(default)]
78 pub timestamp: u64,
79 pub identifier: String,
81 #[serde(default)]
83 pub edited_at: u64,
84 #[serde(default)]
86 pub download: bool,
87 #[serde(default)]
89 pub public: bool,
90 #[serde(default)]
92 pub description: String,
93 #[serde(default)]
95 pub files: u64,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct AlbumsList {
104 pub success: bool,
106 pub albums: Vec<Album>,
108 pub home_domain: Option<Url>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
114pub struct AlbumFile {
115 pub id: u64,
116 pub name: String,
117 #[serde(rename = "userid", deserialize_with = "de_string_or_number")]
118 pub user_id: String,
119 #[serde(deserialize_with = "de_u64_or_string")]
120 pub size: u64,
121 pub timestamp: u64,
122 #[serde(rename = "last_visited_at")]
123 pub last_visited_at: Option<String>,
124 pub slug: String,
125 pub image: String,
127 pub expirydate: Option<String>,
129 #[serde(rename = "albumid", deserialize_with = "de_string_or_number")]
130 pub album_id: String,
131 pub extname: String,
132 pub thumb: String,
134}
135
136fn de_string_or_number<'de, D>(deserializer: D) -> Result<String, D::Error>
137where
138 D: serde::Deserializer<'de>,
139{
140 struct StringOrNumber;
141
142 impl<'de> Visitor<'de> for StringOrNumber {
143 type Value = String;
144
145 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
146 formatter.write_str("a string or number")
147 }
148
149 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
150 where
151 E: de::Error,
152 {
153 Ok(v.to_string())
154 }
155
156 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
157 where
158 E: de::Error,
159 {
160 Ok(v)
161 }
162
163 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
164 where
165 E: de::Error,
166 {
167 Ok(v.to_string())
168 }
169
170 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
171 where
172 E: de::Error,
173 {
174 Ok(v.to_string())
175 }
176 }
177
178 deserializer.deserialize_any(StringOrNumber)
179}
180
181fn de_u64_or_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
182where
183 D: serde::Deserializer<'de>,
184{
185 struct U64OrString;
186
187 impl<'de> Visitor<'de> for U64OrString {
188 type Value = u64;
189
190 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
191 formatter.write_str("a u64 or numeric string")
192 }
193
194 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
195 where
196 E: de::Error,
197 {
198 Ok(v)
199 }
200
201 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
202 where
203 E: de::Error,
204 {
205 if v < 0 {
206 return Err(E::custom("negative value not allowed"));
207 }
208 Ok(v as u64)
209 }
210
211 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
212 where
213 E: de::Error,
214 {
215 v.parse::<u64>().map_err(E::custom)
216 }
217
218 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
219 where
220 E: de::Error,
221 {
222 v.parse::<u64>().map_err(E::custom)
223 }
224 }
225
226 deserializer.deserialize_any(U64OrString)
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct AlbumFilesPage {
235 pub success: bool,
237 pub files: Vec<AlbumFile>,
239 pub count: u64,
241 pub albums: HashMap<String, String>,
243 pub base_domain: Option<Url>,
247}
248
249#[derive(Debug, Serialize)]
250#[serde(rename_all = "camelCase")]
251pub struct CreateAlbumRequest {
252 pub name: String,
253 pub description: Option<String>,
254}
255
256#[derive(Debug, Deserialize)]
257pub struct CreateAlbumResponse {
258 pub success: Option<bool>,
259 pub id: Option<u64>,
260 pub message: Option<String>,
261 pub description: Option<String>,
262}
263
264#[derive(Debug, Deserialize)]
265pub struct UploadResponse {
266 pub success: Option<bool>,
267 pub description: Option<String>,
268 pub files: Option<Vec<UploadedFile>>,
269}
270
271#[derive(Debug, Serialize)]
272#[serde(rename_all = "camelCase")]
273pub(crate) struct EditAlbumRequest {
274 pub(crate) id: u64,
275 pub(crate) name: String,
276 pub(crate) description: String,
277 pub(crate) download: bool,
278 pub(crate) public: bool,
279 #[serde(rename = "requestLink")]
280 pub(crate) request_link: bool,
281}
282
283#[derive(Debug, Deserialize)]
284pub(crate) struct EditAlbumResponse {
285 pub(crate) success: Option<bool>,
286 pub(crate) name: Option<String>,
287 pub(crate) identifier: Option<String>,
288 pub(crate) message: Option<String>,
289 pub(crate) description: Option<String>,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct EditAlbumResult {
295 pub name: Option<String>,
297 pub identifier: Option<String>,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
303pub struct UploadedFile {
304 pub name: String,
306 pub url: String,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct UploadProgress {
313 pub file_name: String,
314 pub bytes_sent: u64,
315 pub total_bytes: u64,
316}
317
318#[derive(Debug, Serialize)]
319pub(crate) struct LoginRequest {
320 pub(crate) username: String,
321 pub(crate) password: String,
322}
323
324#[derive(Debug, Deserialize)]
325pub(crate) struct LoginResponse {
326 pub(crate) token: Option<AuthToken>,
327}
328
329#[derive(Debug, Serialize)]
330pub(crate) struct RegisterRequest {
331 pub(crate) username: String,
332 pub(crate) password: String,
333}
334
335#[derive(Debug, Deserialize)]
336pub(crate) struct RegisterResponse {
337 pub(crate) success: Option<bool>,
338 pub(crate) token: Option<AuthToken>,
339 pub(crate) message: Option<String>,
340 pub(crate) description: Option<String>,
341}
342
343#[derive(Debug, Deserialize)]
344pub(crate) struct NodeResponse {
345 pub(crate) success: Option<bool>,
346 pub(crate) url: Option<String>,
347 pub(crate) message: Option<String>,
348 pub(crate) description: Option<String>,
349}
350
351#[derive(Debug, Serialize)]
352pub(crate) struct VerifyTokenRequest {
353 pub(crate) token: String,
354}
355
356#[derive(Debug, Deserialize)]
357pub(crate) struct VerifyTokenResponse {
358 pub(crate) success: Option<bool>,
359 pub(crate) username: Option<String>,
360 pub(crate) permissions: Option<Permissions>,
361}
362
363#[derive(Debug, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub(crate) struct AlbumsResponse {
366 pub(crate) success: Option<bool>,
367 pub(crate) albums: Option<Vec<Album>>,
368 pub(crate) home_domain: Option<String>,
369}
370
371#[derive(Debug, Deserialize)]
372pub(crate) struct AlbumFilesResponse {
373 pub(crate) success: Option<bool>,
374 pub(crate) files: Option<Vec<AlbumFile>>,
375 pub(crate) count: Option<u64>,
376 pub(crate) albums: Option<HashMap<String, String>>,
377 pub(crate) basedomain: Option<String>,
378 pub(crate) message: Option<String>,
379 pub(crate) description: Option<String>,
380}
381
382impl TryFrom<LoginResponse> for AuthToken {
383 type Error = CyberdropError;
384
385 fn try_from(response: LoginResponse) -> Result<Self, Self::Error> {
386 response.token.ok_or(CyberdropError::MissingToken)
387 }
388}
389
390impl TryFrom<RegisterResponse> for AuthToken {
391 type Error = CyberdropError;
392
393 fn try_from(body: RegisterResponse) -> Result<Self, Self::Error> {
394 if body.success.unwrap_or(false) {
395 return body.token.ok_or(CyberdropError::MissingToken);
396 }
397
398 let msg = body
399 .description
400 .or(body.message)
401 .unwrap_or_else(|| "registration failed".to_string());
402
403 Err(CyberdropError::Api(msg))
404 }
405}
406
407impl TryFrom<VerifyTokenResponse> for TokenVerification {
408 type Error = CyberdropError;
409
410 fn try_from(body: VerifyTokenResponse) -> Result<Self, Self::Error> {
411 let success = body.success.ok_or(CyberdropError::MissingField(
412 "verification response missing success",
413 ))?;
414 let username = body.username.ok_or(CyberdropError::MissingField(
415 "verification response missing username",
416 ))?;
417 let permissions = body.permissions.ok_or(CyberdropError::MissingField(
418 "verification response missing permissions",
419 ))?;
420
421 Ok(TokenVerification {
422 success,
423 username,
424 permissions,
425 })
426 }
427}
428
429impl TryFrom<AlbumsResponse> for AlbumsList {
430 type Error = CyberdropError;
431
432 fn try_from(body: AlbumsResponse) -> Result<Self, Self::Error> {
433 if !body.success.unwrap_or(false) {
434 return Err(CyberdropError::Api("failed to fetch albums".into()));
435 }
436
437 let albums = body.albums.ok_or(CyberdropError::MissingField(
438 "albums response missing albums",
439 ))?;
440
441 let home_domain = match body.home_domain {
442 Some(url) => Some(Url::parse(&url)?),
443 None => None,
444 };
445
446 Ok(AlbumsList {
447 success: true,
448 albums,
449 home_domain,
450 })
451 }
452}
453
454impl TryFrom<AlbumFilesResponse> for AlbumFilesPage {
455 type Error = CyberdropError;
456
457 fn try_from(body: AlbumFilesResponse) -> Result<Self, Self::Error> {
458 if !body.success.unwrap_or(false) {
459 let msg = body
460 .description
461 .or(body.message)
462 .unwrap_or_else(|| "failed to fetch album files".to_string());
463 return Err(CyberdropError::Api(msg));
464 }
465
466 let files = body.files.ok_or(CyberdropError::MissingField(
467 "album files response missing files",
468 ))?;
469
470 let count = body.count.ok_or(CyberdropError::MissingField(
471 "album files response missing count",
472 ))?;
473
474 let base_domain = if files.is_empty() {
475 match body.basedomain {
476 Some(url) => Some(Url::parse(&url)?),
477 None => None,
478 }
479 } else {
480 let url = body.basedomain.ok_or(CyberdropError::MissingField(
481 "album files response missing basedomain",
482 ))?;
483 Some(Url::parse(&url)?)
484 };
485
486 Ok(AlbumFilesPage {
487 success: true,
488 files,
489 count,
490 albums: body.albums.unwrap_or_default(),
491 base_domain,
492 })
493 }
494}
495
496impl TryFrom<CreateAlbumResponse> for u64 {
497 type Error = CyberdropError;
498
499 fn try_from(body: CreateAlbumResponse) -> Result<Self, Self::Error> {
500 if body.success.unwrap_or(false) {
501 return body.id.ok_or(CyberdropError::MissingField(
502 "create album response missing id",
503 ));
504 }
505
506 let msg = body
507 .description
508 .or(body.message)
509 .unwrap_or_else(|| "create album failed".to_string());
510
511 if msg.to_lowercase().contains("already an album") {
512 Err(CyberdropError::AlbumAlreadyExists(msg))
513 } else {
514 Err(CyberdropError::Api(msg))
515 }
516 }
517}
518
519impl TryFrom<UploadResponse> for UploadedFile {
520 type Error = CyberdropError;
521
522 fn try_from(body: UploadResponse) -> Result<Self, Self::Error> {
523 if body.success.unwrap_or(false) {
524 let first = body.files.and_then(|mut files| files.pop()).ok_or(
525 CyberdropError::MissingField("upload response missing files"),
526 )?;
527 let url = Url::parse(&first.url)?;
528 Ok(UploadedFile {
529 name: first.name,
530 url: url.to_string(),
531 })
532 } else {
533 let msg = body
534 .description
535 .unwrap_or_else(|| "upload failed".to_string());
536 Err(CyberdropError::Api(msg))
537 }
538 }
539}
540
541impl TryFrom<EditAlbumResponse> for EditAlbumResult {
542 type Error = CyberdropError;
543
544 fn try_from(body: EditAlbumResponse) -> Result<Self, Self::Error> {
545 if !body.success.unwrap_or(false) {
546 let msg = body
547 .description
548 .or(body.message)
549 .unwrap_or_else(|| "edit album failed".to_string());
550 return Err(CyberdropError::Api(msg));
551 }
552
553 if body.name.is_none() && body.identifier.is_none() {
554 return Err(CyberdropError::MissingField(
555 "edit album response missing name/identifier",
556 ));
557 }
558
559 Ok(EditAlbumResult {
560 name: body.name,
561 identifier: body.identifier,
562 })
563 }
564}