1use std::collections::HashMap;
2
3use reqwest::Url;
4use serde::{Deserialize, Serialize};
5
6use crate::CyberdropError;
7
8#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
12#[serde(transparent)]
13pub struct AuthToken {
14 pub(crate) token: String,
15}
16
17impl AuthToken {
18 pub fn new(token: impl Into<String>) -> Self {
20 Self {
21 token: token.into(),
22 }
23 }
24
25 pub fn as_str(&self) -> &str {
27 &self.token
28 }
29
30 pub fn into_string(self) -> String {
32 self.token
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
38pub struct Permissions {
39 pub user: bool,
41 pub poweruser: bool,
43 pub moderator: bool,
45 pub admin: bool,
47 pub superadmin: bool,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct TokenVerification {
54 pub success: bool,
56 pub username: String,
58 pub permissions: Permissions,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Album {
69 pub id: u64,
71 pub name: String,
73 pub timestamp: u64,
75 pub identifier: String,
77 pub edited_at: u64,
79 pub download: bool,
81 pub public: bool,
83 pub description: String,
85 pub files: u64,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct AlbumsList {
95 pub success: bool,
97 pub albums: Vec<Album>,
99 pub home_domain: Option<Url>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
105pub struct AlbumFile {
106 pub id: u64,
107 pub name: String,
108 #[serde(rename = "userid")]
109 pub user_id: String,
110 pub size: u64,
111 pub timestamp: u64,
112 #[serde(rename = "last_visited_at")]
113 pub last_visited_at: Option<String>,
114 pub slug: String,
115 pub image: String,
117 pub expirydate: Option<String>,
119 #[serde(rename = "albumid")]
120 pub album_id: String,
121 pub extname: String,
122 pub thumb: String,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct AlbumFilesPage {
132 pub success: bool,
134 pub files: Vec<AlbumFile>,
136 pub count: u64,
138 pub albums: HashMap<String, String>,
140 pub base_domain: Url,
142}
143
144#[derive(Debug, Serialize)]
145#[serde(rename_all = "camelCase")]
146pub struct CreateAlbumRequest {
147 pub name: String,
148 pub description: Option<String>,
149}
150
151#[derive(Debug, Deserialize)]
152pub struct CreateAlbumResponse {
153 pub success: Option<bool>,
154 pub id: Option<u64>,
155 pub message: Option<String>,
156 pub description: Option<String>,
157}
158
159#[derive(Debug, Deserialize)]
160pub struct UploadResponse {
161 pub success: Option<bool>,
162 pub description: Option<String>,
163 pub files: Option<Vec<UploadedFile>>,
164}
165
166#[derive(Debug, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub(crate) struct EditAlbumRequest {
169 pub(crate) id: u64,
170 pub(crate) name: String,
171 pub(crate) description: String,
172 pub(crate) download: bool,
173 pub(crate) public: bool,
174 #[serde(rename = "requestLink")]
175 pub(crate) request_link: bool,
176}
177
178#[derive(Debug, Deserialize)]
179pub(crate) struct EditAlbumResponse {
180 pub(crate) success: Option<bool>,
181 pub(crate) name: Option<String>,
182 pub(crate) identifier: Option<String>,
183 pub(crate) message: Option<String>,
184 pub(crate) description: Option<String>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct EditAlbumResult {
190 pub name: Option<String>,
192 pub identifier: Option<String>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
198pub struct UploadedFile {
199 pub name: String,
201 pub url: String,
203}
204
205#[derive(Debug, Serialize)]
206pub(crate) struct LoginRequest {
207 pub(crate) username: String,
208 pub(crate) password: String,
209}
210
211#[derive(Debug, Deserialize)]
212pub(crate) struct LoginResponse {
213 pub(crate) token: Option<AuthToken>,
214}
215
216#[derive(Debug, Serialize)]
217pub(crate) struct VerifyTokenRequest {
218 pub(crate) token: String,
219}
220
221#[derive(Debug, Deserialize)]
222pub(crate) struct VerifyTokenResponse {
223 pub(crate) success: Option<bool>,
224 pub(crate) username: Option<String>,
225 pub(crate) permissions: Option<Permissions>,
226}
227
228#[derive(Debug, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub(crate) struct AlbumsResponse {
231 pub(crate) success: Option<bool>,
232 pub(crate) albums: Option<Vec<Album>>,
233 pub(crate) home_domain: Option<String>,
234}
235
236#[derive(Debug, Deserialize)]
237pub(crate) struct AlbumFilesResponse {
238 pub(crate) success: Option<bool>,
239 pub(crate) files: Option<Vec<AlbumFile>>,
240 pub(crate) count: Option<u64>,
241 pub(crate) albums: Option<HashMap<String, String>>,
242 pub(crate) basedomain: Option<String>,
243 pub(crate) message: Option<String>,
244 pub(crate) description: Option<String>,
245}
246
247impl TryFrom<LoginResponse> for AuthToken {
248 type Error = CyberdropError;
249
250 fn try_from(response: LoginResponse) -> Result<Self, Self::Error> {
251 response.token.ok_or(CyberdropError::MissingToken)
252 }
253}
254
255impl TryFrom<VerifyTokenResponse> for TokenVerification {
256 type Error = CyberdropError;
257
258 fn try_from(body: VerifyTokenResponse) -> Result<Self, Self::Error> {
259 let success = body.success.ok_or(CyberdropError::MissingField(
260 "verification response missing success",
261 ))?;
262 let username = body.username.ok_or(CyberdropError::MissingField(
263 "verification response missing username",
264 ))?;
265 let permissions = body.permissions.ok_or(CyberdropError::MissingField(
266 "verification response missing permissions",
267 ))?;
268
269 Ok(TokenVerification {
270 success,
271 username,
272 permissions,
273 })
274 }
275}
276
277impl TryFrom<AlbumsResponse> for AlbumsList {
278 type Error = CyberdropError;
279
280 fn try_from(body: AlbumsResponse) -> Result<Self, Self::Error> {
281 if !body.success.unwrap_or(false) {
282 return Err(CyberdropError::Api("failed to fetch albums".into()));
283 }
284
285 let albums = body.albums.ok_or(CyberdropError::MissingField(
286 "albums response missing albums",
287 ))?;
288
289 let home_domain = match body.home_domain {
290 Some(url) => Some(Url::parse(&url)?),
291 None => None,
292 };
293
294 Ok(AlbumsList {
295 success: true,
296 albums,
297 home_domain,
298 })
299 }
300}
301
302impl TryFrom<AlbumFilesResponse> for AlbumFilesPage {
303 type Error = CyberdropError;
304
305 fn try_from(body: AlbumFilesResponse) -> Result<Self, Self::Error> {
306 if !body.success.unwrap_or(false) {
307 let msg = body
308 .description
309 .or(body.message)
310 .unwrap_or_else(|| "failed to fetch album files".to_string());
311 return Err(CyberdropError::Api(msg));
312 }
313
314 let files = body.files.ok_or(CyberdropError::MissingField(
315 "album files response missing files",
316 ))?;
317
318 let count = body.count.ok_or(CyberdropError::MissingField(
319 "album files response missing count",
320 ))?;
321
322 let base_domain = body
323 .basedomain
324 .ok_or(CyberdropError::MissingField(
325 "album files response missing basedomain",
326 ))
327 .and_then(|url| Ok(Url::parse(&url)?))?;
328
329 Ok(AlbumFilesPage {
330 success: true,
331 files,
332 count,
333 albums: body.albums.unwrap_or_default(),
334 base_domain,
335 })
336 }
337}
338
339impl TryFrom<CreateAlbumResponse> for u64 {
340 type Error = CyberdropError;
341
342 fn try_from(body: CreateAlbumResponse) -> Result<Self, Self::Error> {
343 if body.success.unwrap_or(false) {
344 return body.id.ok_or(CyberdropError::MissingField(
345 "create album response missing id",
346 ));
347 }
348
349 let msg = body
350 .description
351 .or(body.message)
352 .unwrap_or_else(|| "create album failed".to_string());
353
354 if msg.to_lowercase().contains("already an album") {
355 Err(CyberdropError::AlbumAlreadyExists(msg))
356 } else {
357 Err(CyberdropError::Api(msg))
358 }
359 }
360}
361
362impl TryFrom<UploadResponse> for UploadedFile {
363 type Error = CyberdropError;
364
365 fn try_from(body: UploadResponse) -> Result<Self, Self::Error> {
366 if body.success.unwrap_or(false) {
367 let first = body.files.and_then(|mut files| files.pop()).ok_or(
368 CyberdropError::MissingField("upload response missing files"),
369 )?;
370 let url = Url::parse(&first.url)?;
371 Ok(UploadedFile {
372 name: first.name,
373 url: url.to_string(),
374 })
375 } else {
376 let msg = body
377 .description
378 .unwrap_or_else(|| "upload failed".to_string());
379 Err(CyberdropError::Api(msg))
380 }
381 }
382}
383
384impl TryFrom<EditAlbumResponse> for EditAlbumResult {
385 type Error = CyberdropError;
386
387 fn try_from(body: EditAlbumResponse) -> Result<Self, Self::Error> {
388 if !body.success.unwrap_or(false) {
389 let msg = body
390 .description
391 .or(body.message)
392 .unwrap_or_else(|| "edit album failed".to_string());
393 return Err(CyberdropError::Api(msg));
394 }
395
396 if body.name.is_none() && body.identifier.is_none() {
397 return Err(CyberdropError::MissingField(
398 "edit album response missing name/identifier",
399 ));
400 }
401
402 Ok(EditAlbumResult {
403 name: body.name,
404 identifier: body.identifier,
405 })
406 }
407}