1use std::collections::HashMap;
2
3use reqwest::Url;
4use serde::{Deserialize, Serialize};
5
6use crate::CyberdropError;
7
8#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
13#[serde(transparent)]
14pub struct AuthToken {
15 pub(crate) token: String,
16}
17
18impl AuthToken {
19 pub fn new(token: impl Into<String>) -> Self {
21 Self {
22 token: token.into(),
23 }
24 }
25
26 pub fn as_str(&self) -> &str {
28 &self.token
29 }
30
31 pub fn into_string(self) -> String {
33 self.token
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
39pub struct Permissions {
40 pub user: bool,
42 pub poweruser: bool,
44 pub moderator: bool,
46 pub admin: bool,
48 pub superadmin: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct TokenVerification {
55 pub success: bool,
57 pub username: String,
59 pub permissions: Permissions,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Album {
70 pub id: u64,
72 pub name: String,
74 pub timestamp: u64,
76 pub identifier: String,
78 pub edited_at: u64,
80 pub download: bool,
82 pub public: bool,
84 pub description: String,
86 pub files: u64,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct AlbumsList {
96 pub success: bool,
98 pub albums: Vec<Album>,
100 pub home_domain: Option<Url>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
106pub struct AlbumFile {
107 pub id: u64,
108 pub name: String,
109 #[serde(rename = "userid")]
110 pub user_id: String,
111 pub size: u64,
112 pub timestamp: u64,
113 #[serde(rename = "last_visited_at")]
114 pub last_visited_at: Option<String>,
115 pub slug: String,
116 pub image: String,
118 pub expirydate: Option<String>,
120 #[serde(rename = "albumid")]
121 pub album_id: String,
122 pub extname: String,
123 pub thumb: String,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct AlbumFilesPage {
133 pub success: bool,
135 pub files: Vec<AlbumFile>,
137 pub count: u64,
139 pub albums: HashMap<String, String>,
141 pub base_domain: Option<Url>,
145}
146
147#[derive(Debug, Serialize)]
148#[serde(rename_all = "camelCase")]
149pub struct CreateAlbumRequest {
150 pub name: String,
151 pub description: Option<String>,
152}
153
154#[derive(Debug, Deserialize)]
155pub struct CreateAlbumResponse {
156 pub success: Option<bool>,
157 pub id: Option<u64>,
158 pub message: Option<String>,
159 pub description: Option<String>,
160}
161
162#[derive(Debug, Deserialize)]
163pub struct UploadResponse {
164 pub success: Option<bool>,
165 pub description: Option<String>,
166 pub files: Option<Vec<UploadedFile>>,
167}
168
169#[derive(Debug, Serialize)]
170#[serde(rename_all = "camelCase")]
171pub(crate) struct EditAlbumRequest {
172 pub(crate) id: u64,
173 pub(crate) name: String,
174 pub(crate) description: String,
175 pub(crate) download: bool,
176 pub(crate) public: bool,
177 #[serde(rename = "requestLink")]
178 pub(crate) request_link: bool,
179}
180
181#[derive(Debug, Deserialize)]
182pub(crate) struct EditAlbumResponse {
183 pub(crate) success: Option<bool>,
184 pub(crate) name: Option<String>,
185 pub(crate) identifier: Option<String>,
186 pub(crate) message: Option<String>,
187 pub(crate) description: Option<String>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct EditAlbumResult {
193 pub name: Option<String>,
195 pub identifier: Option<String>,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
201pub struct UploadedFile {
202 pub name: String,
204 pub url: String,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct UploadProgress {
211 pub file_name: String,
212 pub bytes_sent: u64,
213 pub total_bytes: u64,
214}
215
216#[derive(Debug, Serialize)]
217pub(crate) struct LoginRequest {
218 pub(crate) username: String,
219 pub(crate) password: String,
220}
221
222#[derive(Debug, Deserialize)]
223pub(crate) struct LoginResponse {
224 pub(crate) token: Option<AuthToken>,
225}
226
227#[derive(Debug, Serialize)]
228pub(crate) struct RegisterRequest {
229 pub(crate) username: String,
230 pub(crate) password: String,
231}
232
233#[derive(Debug, Deserialize)]
234pub(crate) struct RegisterResponse {
235 pub(crate) success: Option<bool>,
236 pub(crate) token: Option<AuthToken>,
237 pub(crate) message: Option<String>,
238 pub(crate) description: Option<String>,
239}
240
241#[derive(Debug, Deserialize)]
242pub(crate) struct NodeResponse {
243 pub(crate) success: Option<bool>,
244 pub(crate) url: Option<String>,
245 pub(crate) message: Option<String>,
246 pub(crate) description: Option<String>,
247}
248
249#[derive(Debug, Serialize)]
250pub(crate) struct VerifyTokenRequest {
251 pub(crate) token: String,
252}
253
254#[derive(Debug, Deserialize)]
255pub(crate) struct VerifyTokenResponse {
256 pub(crate) success: Option<bool>,
257 pub(crate) username: Option<String>,
258 pub(crate) permissions: Option<Permissions>,
259}
260
261#[derive(Debug, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub(crate) struct AlbumsResponse {
264 pub(crate) success: Option<bool>,
265 pub(crate) albums: Option<Vec<Album>>,
266 pub(crate) home_domain: Option<String>,
267}
268
269#[derive(Debug, Deserialize)]
270pub(crate) struct AlbumFilesResponse {
271 pub(crate) success: Option<bool>,
272 pub(crate) files: Option<Vec<AlbumFile>>,
273 pub(crate) count: Option<u64>,
274 pub(crate) albums: Option<HashMap<String, String>>,
275 pub(crate) basedomain: Option<String>,
276 pub(crate) message: Option<String>,
277 pub(crate) description: Option<String>,
278}
279
280impl TryFrom<LoginResponse> for AuthToken {
281 type Error = CyberdropError;
282
283 fn try_from(response: LoginResponse) -> Result<Self, Self::Error> {
284 response.token.ok_or(CyberdropError::MissingToken)
285 }
286}
287
288impl TryFrom<RegisterResponse> for AuthToken {
289 type Error = CyberdropError;
290
291 fn try_from(body: RegisterResponse) -> Result<Self, Self::Error> {
292 if body.success.unwrap_or(false) {
293 return body.token.ok_or(CyberdropError::MissingToken);
294 }
295
296 let msg = body
297 .description
298 .or(body.message)
299 .unwrap_or_else(|| "registration failed".to_string());
300
301 Err(CyberdropError::Api(msg))
302 }
303}
304
305impl TryFrom<VerifyTokenResponse> for TokenVerification {
306 type Error = CyberdropError;
307
308 fn try_from(body: VerifyTokenResponse) -> Result<Self, Self::Error> {
309 let success = body.success.ok_or(CyberdropError::MissingField(
310 "verification response missing success",
311 ))?;
312 let username = body.username.ok_or(CyberdropError::MissingField(
313 "verification response missing username",
314 ))?;
315 let permissions = body.permissions.ok_or(CyberdropError::MissingField(
316 "verification response missing permissions",
317 ))?;
318
319 Ok(TokenVerification {
320 success,
321 username,
322 permissions,
323 })
324 }
325}
326
327impl TryFrom<AlbumsResponse> for AlbumsList {
328 type Error = CyberdropError;
329
330 fn try_from(body: AlbumsResponse) -> Result<Self, Self::Error> {
331 if !body.success.unwrap_or(false) {
332 return Err(CyberdropError::Api("failed to fetch albums".into()));
333 }
334
335 let albums = body.albums.ok_or(CyberdropError::MissingField(
336 "albums response missing albums",
337 ))?;
338
339 let home_domain = match body.home_domain {
340 Some(url) => Some(Url::parse(&url)?),
341 None => None,
342 };
343
344 Ok(AlbumsList {
345 success: true,
346 albums,
347 home_domain,
348 })
349 }
350}
351
352impl TryFrom<AlbumFilesResponse> for AlbumFilesPage {
353 type Error = CyberdropError;
354
355 fn try_from(body: AlbumFilesResponse) -> Result<Self, Self::Error> {
356 if !body.success.unwrap_or(false) {
357 let msg = body
358 .description
359 .or(body.message)
360 .unwrap_or_else(|| "failed to fetch album files".to_string());
361 return Err(CyberdropError::Api(msg));
362 }
363
364 let files = body.files.ok_or(CyberdropError::MissingField(
365 "album files response missing files",
366 ))?;
367
368 let count = body.count.ok_or(CyberdropError::MissingField(
369 "album files response missing count",
370 ))?;
371
372 let base_domain = if files.is_empty() {
373 match body.basedomain {
374 Some(url) => Some(Url::parse(&url)?),
375 None => None,
376 }
377 } else {
378 let url = body.basedomain.ok_or(CyberdropError::MissingField(
379 "album files response missing basedomain",
380 ))?;
381 Some(Url::parse(&url)?)
382 };
383
384 Ok(AlbumFilesPage {
385 success: true,
386 files,
387 count,
388 albums: body.albums.unwrap_or_default(),
389 base_domain,
390 })
391 }
392}
393
394impl TryFrom<CreateAlbumResponse> for u64 {
395 type Error = CyberdropError;
396
397 fn try_from(body: CreateAlbumResponse) -> Result<Self, Self::Error> {
398 if body.success.unwrap_or(false) {
399 return body.id.ok_or(CyberdropError::MissingField(
400 "create album response missing id",
401 ));
402 }
403
404 let msg = body
405 .description
406 .or(body.message)
407 .unwrap_or_else(|| "create album failed".to_string());
408
409 if msg.to_lowercase().contains("already an album") {
410 Err(CyberdropError::AlbumAlreadyExists(msg))
411 } else {
412 Err(CyberdropError::Api(msg))
413 }
414 }
415}
416
417impl TryFrom<UploadResponse> for UploadedFile {
418 type Error = CyberdropError;
419
420 fn try_from(body: UploadResponse) -> Result<Self, Self::Error> {
421 if body.success.unwrap_or(false) {
422 let first = body.files.and_then(|mut files| files.pop()).ok_or(
423 CyberdropError::MissingField("upload response missing files"),
424 )?;
425 let url = Url::parse(&first.url)?;
426 Ok(UploadedFile {
427 name: first.name,
428 url: url.to_string(),
429 })
430 } else {
431 let msg = body
432 .description
433 .unwrap_or_else(|| "upload failed".to_string());
434 Err(CyberdropError::Api(msg))
435 }
436 }
437}
438
439impl TryFrom<EditAlbumResponse> for EditAlbumResult {
440 type Error = CyberdropError;
441
442 fn try_from(body: EditAlbumResponse) -> Result<Self, Self::Error> {
443 if !body.success.unwrap_or(false) {
444 let msg = body
445 .description
446 .or(body.message)
447 .unwrap_or_else(|| "edit album failed".to_string());
448 return Err(CyberdropError::Api(msg));
449 }
450
451 if body.name.is_none() && body.identifier.is_none() {
452 return Err(CyberdropError::MissingField(
453 "edit album response missing name/identifier",
454 ));
455 }
456
457 Ok(EditAlbumResult {
458 name: body.name,
459 identifier: body.identifier,
460 })
461 }
462}