use reqwest::{
Client, Method, RequestBuilder, StatusCode, Url,
header::{ACCEPT, ACCEPT_LANGUAGE},
multipart::Form,
};
use serde::de::DeserializeOwned;
use crate::CyberdropError;
use crate::config::{DEFAULT_BASE_URL, DEFAULT_TIMEOUT};
use crate::token::AuthToken;
#[derive(Debug, Clone)]
pub struct CyberdropClient {
pub(crate) client: Client,
pub(crate) base_url: Url,
pub(crate) auth_token: Option<AuthToken>,
}
impl CyberdropClient {
pub fn new() -> Result<Self, CyberdropError> {
let client = Client::builder()
.timeout(DEFAULT_TIMEOUT)
.user_agent(default_user_agent())
.build()?;
Ok(Self {
client,
base_url: Url::parse(DEFAULT_BASE_URL)?,
auth_token: None,
})
}
pub fn new_with_token(token: impl Into<String>) -> Result<Self, CyberdropError> {
Ok(Self::new()?.with_auth_token(token))
}
pub fn auth_token(&self) -> Option<&str> {
self.auth_token.as_ref().map(AuthToken::as_str)
}
pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
self.auth_token = Some(AuthToken::new(token));
self
}
pub(crate) async fn get_json<T>(
&self,
path: &str,
requires_auth: bool,
) -> Result<T, CyberdropError>
where
T: DeserializeOwned,
{
let builder = self.build_request(Method::GET, path, requires_auth)?;
self.send_json(builder).await
}
pub(crate) async fn get_json_with_header<T>(
&self,
path: &str,
requires_auth: bool,
header_name: &'static str,
header_value: &'static str,
) -> Result<T, CyberdropError>
where
T: DeserializeOwned,
{
let builder = self
.build_request(Method::GET, path, requires_auth)?
.header(header_name, header_value);
self.send_json(builder).await
}
pub(crate) async fn post_json<B, T>(
&self,
path: &str,
body: &B,
requires_auth: bool,
) -> Result<T, CyberdropError>
where
B: serde::Serialize + ?Sized,
T: DeserializeOwned,
{
let builder = self
.build_request(Method::POST, path, requires_auth)?
.json(body);
self.send_json(builder).await
}
pub(crate) async fn post_upload_json_url<B, T>(
&self,
url: Url,
body: &B,
) -> Result<T, CyberdropError>
where
B: serde::Serialize + ?Sized,
T: DeserializeOwned,
{
let builder = self
.upload_headers(self.build_request_url(Method::POST, url, true)?)
.json(body);
self.send_json(builder).await
}
pub(crate) async fn post_upload_multipart_url<T>(
&self,
url: Url,
form: Form,
album_id: Option<u64>,
) -> Result<T, CyberdropError>
where
T: DeserializeOwned,
{
let mut builder = self.upload_headers(self.build_request_url(Method::POST, url, true)?);
if let Some(id) = album_id {
builder = builder.header("albumid", id);
}
self.send_json(builder.multipart(form)).await
}
async fn send_json<T>(&self, builder: RequestBuilder) -> Result<T, CyberdropError>
where
T: DeserializeOwned,
{
let response = builder.send().await?;
Self::map_status(response.status())?;
Ok(response.json().await?)
}
fn build_request(
&self,
method: Method,
path: &str,
requires_auth: bool,
) -> Result<RequestBuilder, CyberdropError> {
let url = self.base_url.join(path)?;
self.build_request_url(method, url, requires_auth)
}
fn build_request_url(
&self,
method: Method,
url: Url,
requires_auth: bool,
) -> Result<RequestBuilder, CyberdropError> {
let builder = self
.client
.request(method, url)
.header(ACCEPT, "application/json, text/plain, */*")
.header(ACCEPT_LANGUAGE, "nl,en-US;q=0.9,en;q=0.8");
if requires_auth {
self.apply_auth(builder)
} else {
Ok(builder)
}
}
fn map_status(status: StatusCode) -> Result<(), CyberdropError> {
if status.is_success() {
return Ok(());
}
if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
Err(CyberdropError::AuthenticationFailed(status))
} else {
Err(CyberdropError::RequestFailed(status))
}
}
fn apply_auth(&self, builder: RequestBuilder) -> Result<RequestBuilder, CyberdropError> {
let token = self
.auth_token
.as_ref()
.ok_or(CyberdropError::MissingAuthToken)?;
Ok(builder.header("token", token.as_str()))
}
fn upload_headers(&self, builder: RequestBuilder) -> RequestBuilder {
let origin = self.base_url.origin().ascii_serialization();
let referer = self.base_url.as_str();
builder
.header("X-Requested-With", "XMLHttpRequest")
.header("striptags", "undefined")
.header("Origin", origin)
.header("Referer", referer)
.header("Cache-Control", "no-cache")
.header("Pragma", "no-cache")
}
}
fn default_user_agent() -> String {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0".into()
}