use std::collections::HashMap;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde::de::{self, Visitor};
use std::fmt;
use crate::CyberdropError;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(transparent)]
pub struct AuthToken {
pub(crate) token: String,
}
impl AuthToken {
pub fn new(token: impl Into<String>) -> Self {
Self {
token: token.into(),
}
}
pub fn as_str(&self) -> &str {
&self.token
}
pub fn into_string(self) -> String {
self.token
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Permissions {
pub user: bool,
pub poweruser: bool,
pub moderator: bool,
pub admin: bool,
pub superadmin: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenVerification {
pub success: bool,
pub username: String,
pub permissions: Permissions,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Album {
pub id: u64,
pub name: String,
#[serde(default)]
pub timestamp: u64,
pub identifier: String,
#[serde(default)]
pub edited_at: u64,
#[serde(default)]
pub download: bool,
#[serde(default)]
pub public: bool,
#[serde(default)]
pub description: String,
#[serde(default)]
pub files: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlbumsList {
pub success: bool,
pub albums: Vec<Album>,
pub home_domain: Option<Url>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct AlbumFile {
pub id: u64,
pub name: String,
#[serde(rename = "userid", deserialize_with = "de_string_or_number")]
pub user_id: String,
#[serde(deserialize_with = "de_u64_or_string")]
pub size: u64,
pub timestamp: u64,
#[serde(rename = "last_visited_at")]
pub last_visited_at: Option<String>,
pub slug: String,
pub image: String,
pub expirydate: Option<String>,
#[serde(rename = "albumid", deserialize_with = "de_string_or_number")]
pub album_id: String,
pub extname: String,
pub thumb: String,
}
fn de_string_or_number<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
struct StringOrNumber;
impl<'de> Visitor<'de> for StringOrNumber {
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or number")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.to_string())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.to_string())
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.to_string())
}
}
deserializer.deserialize_any(StringOrNumber)
}
fn de_u64_or_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
struct U64OrString;
impl<'de> Visitor<'de> for U64OrString {
type Value = u64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a u64 or numeric string")
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
if v < 0 {
return Err(E::custom("negative value not allowed"));
}
Ok(v as u64)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
v.parse::<u64>().map_err(E::custom)
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
v.parse::<u64>().map_err(E::custom)
}
}
deserializer.deserialize_any(U64OrString)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlbumFilesPage {
pub success: bool,
pub files: Vec<AlbumFile>,
pub count: u64,
pub albums: HashMap<String, String>,
pub base_domain: Option<Url>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAlbumRequest {
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CreateAlbumResponse {
pub success: Option<bool>,
pub id: Option<u64>,
pub message: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UploadResponse {
pub success: Option<bool>,
pub description: Option<String>,
pub files: Option<Vec<UploadedFile>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EditAlbumRequest {
pub(crate) id: u64,
pub(crate) name: String,
pub(crate) description: String,
pub(crate) download: bool,
pub(crate) public: bool,
#[serde(rename = "requestLink")]
pub(crate) request_link: bool,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EditAlbumResponse {
pub(crate) success: Option<bool>,
pub(crate) name: Option<String>,
pub(crate) identifier: Option<String>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditAlbumResult {
pub name: Option<String>,
pub identifier: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct UploadedFile {
pub name: String,
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UploadProgress {
pub file_name: String,
pub bytes_sent: u64,
pub total_bytes: u64,
}
#[derive(Debug, Serialize)]
pub(crate) struct LoginRequest {
pub(crate) username: String,
pub(crate) password: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LoginResponse {
pub(crate) token: Option<AuthToken>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RegisterRequest {
pub(crate) username: String,
pub(crate) password: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct RegisterResponse {
pub(crate) success: Option<bool>,
pub(crate) token: Option<AuthToken>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct NodeResponse {
pub(crate) success: Option<bool>,
pub(crate) url: Option<String>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct VerifyTokenRequest {
pub(crate) token: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct VerifyTokenResponse {
pub(crate) success: Option<bool>,
pub(crate) username: Option<String>,
pub(crate) permissions: Option<Permissions>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AlbumsResponse {
pub(crate) success: Option<bool>,
pub(crate) albums: Option<Vec<Album>>,
pub(crate) home_domain: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AlbumFilesResponse {
pub(crate) success: Option<bool>,
pub(crate) files: Option<Vec<AlbumFile>>,
pub(crate) count: Option<u64>,
pub(crate) albums: Option<HashMap<String, String>>,
pub(crate) basedomain: Option<String>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
impl TryFrom<LoginResponse> for AuthToken {
type Error = CyberdropError;
fn try_from(response: LoginResponse) -> Result<Self, Self::Error> {
response.token.ok_or(CyberdropError::MissingToken)
}
}
impl TryFrom<RegisterResponse> for AuthToken {
type Error = CyberdropError;
fn try_from(body: RegisterResponse) -> Result<Self, Self::Error> {
if body.success.unwrap_or(false) {
return body.token.ok_or(CyberdropError::MissingToken);
}
let msg = body
.description
.or(body.message)
.unwrap_or_else(|| "registration failed".to_string());
Err(CyberdropError::Api(msg))
}
}
impl TryFrom<VerifyTokenResponse> for TokenVerification {
type Error = CyberdropError;
fn try_from(body: VerifyTokenResponse) -> Result<Self, Self::Error> {
let success = body.success.ok_or(CyberdropError::MissingField(
"verification response missing success",
))?;
let username = body.username.ok_or(CyberdropError::MissingField(
"verification response missing username",
))?;
let permissions = body.permissions.ok_or(CyberdropError::MissingField(
"verification response missing permissions",
))?;
Ok(TokenVerification {
success,
username,
permissions,
})
}
}
impl TryFrom<AlbumsResponse> for AlbumsList {
type Error = CyberdropError;
fn try_from(body: AlbumsResponse) -> Result<Self, Self::Error> {
if !body.success.unwrap_or(false) {
return Err(CyberdropError::Api("failed to fetch albums".into()));
}
let albums = body.albums.ok_or(CyberdropError::MissingField(
"albums response missing albums",
))?;
let home_domain = match body.home_domain {
Some(url) => Some(Url::parse(&url)?),
None => None,
};
Ok(AlbumsList {
success: true,
albums,
home_domain,
})
}
}
impl TryFrom<AlbumFilesResponse> for AlbumFilesPage {
type Error = CyberdropError;
fn try_from(body: AlbumFilesResponse) -> Result<Self, Self::Error> {
if !body.success.unwrap_or(false) {
let msg = body
.description
.or(body.message)
.unwrap_or_else(|| "failed to fetch album files".to_string());
return Err(CyberdropError::Api(msg));
}
let files = body.files.ok_or(CyberdropError::MissingField(
"album files response missing files",
))?;
let count = body.count.ok_or(CyberdropError::MissingField(
"album files response missing count",
))?;
let base_domain = if files.is_empty() {
match body.basedomain {
Some(url) => Some(Url::parse(&url)?),
None => None,
}
} else {
let url = body.basedomain.ok_or(CyberdropError::MissingField(
"album files response missing basedomain",
))?;
Some(Url::parse(&url)?)
};
Ok(AlbumFilesPage {
success: true,
files,
count,
albums: body.albums.unwrap_or_default(),
base_domain,
})
}
}
impl TryFrom<CreateAlbumResponse> for u64 {
type Error = CyberdropError;
fn try_from(body: CreateAlbumResponse) -> Result<Self, Self::Error> {
if body.success.unwrap_or(false) {
return body.id.ok_or(CyberdropError::MissingField(
"create album response missing id",
));
}
let msg = body
.description
.or(body.message)
.unwrap_or_else(|| "create album failed".to_string());
if msg.to_lowercase().contains("already an album") {
Err(CyberdropError::AlbumAlreadyExists(msg))
} else {
Err(CyberdropError::Api(msg))
}
}
}
impl TryFrom<UploadResponse> for UploadedFile {
type Error = CyberdropError;
fn try_from(body: UploadResponse) -> Result<Self, Self::Error> {
if body.success.unwrap_or(false) {
let first = body.files.and_then(|mut files| files.pop()).ok_or(
CyberdropError::MissingField("upload response missing files"),
)?;
let url = Url::parse(&first.url)?;
Ok(UploadedFile {
name: first.name,
url: url.to_string(),
})
} else {
let msg = body
.description
.unwrap_or_else(|| "upload failed".to_string());
Err(CyberdropError::Api(msg))
}
}
}
impl TryFrom<EditAlbumResponse> for EditAlbumResult {
type Error = CyberdropError;
fn try_from(body: EditAlbumResponse) -> Result<Self, Self::Error> {
if !body.success.unwrap_or(false) {
let msg = body
.description
.or(body.message)
.unwrap_or_else(|| "edit album failed".to_string());
return Err(CyberdropError::Api(msg));
}
if body.name.is_none() && body.identifier.is_none() {
return Err(CyberdropError::MissingField(
"edit album response missing name/identifier",
));
}
Ok(EditAlbumResult {
name: body.name,
identifier: body.identifier,
})
}
}