use crate::error::{ApiErrorResponse, WattpadError};
use crate::endpoints::story::StoryClient;
use crate::endpoints::user::UserClient;
use crate::field::{AuthRequiredFields, DefaultableFields};
use bytes::Bytes;
use reqwest::Client as ReqwestClient;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Default)]
pub struct WattpadClientBuilder {
client: Option<ReqwestClient>,
user_agent: Option<String>,
headers: Option<HeaderMap>,
}
impl WattpadClientBuilder {
pub fn reqwest_client(mut self, client: ReqwestClient) -> Self {
self.client = Some(client);
self
}
pub fn user_agent(mut self, user_agent: &str) -> Self {
self.user_agent = Some(user_agent.to_string());
self
}
pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
self.headers.get_or_insert_with(HeaderMap::new).insert(key, value);
self
}
pub fn build(self) -> WattpadClient {
let http_client = match self.client {
Some(client) => client,
None => {
let mut headers = self.headers.unwrap_or_default();
let ua_string = self.user_agent.unwrap_or_else(||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36".to_string()
);
headers.insert(USER_AGENT, HeaderValue::from_str(&ua_string).expect("Invalid User-Agent string"));
let mut client_builder = ReqwestClient::builder()
.default_headers(headers);
#[cfg(not(target_arch = "wasm32"))]
{
client_builder = client_builder.cookie_store(true);
}
client_builder.build()
.expect("Failed to build reqwest client")
}
};
let auth_flag = Arc::new(AtomicBool::new(false));
WattpadClient {
user: UserClient {
http: http_client.clone(),
is_authenticated: auth_flag.clone(),
},
story: StoryClient {
http: http_client.clone(),
is_authenticated: auth_flag.clone(),
},
http: http_client,
is_authenticated: auth_flag,
}
}
}
pub struct WattpadClient {
http: reqwest::Client,
is_authenticated: Arc<AtomicBool>,
pub user: UserClient,
pub story: StoryClient,
}
impl WattpadClient {
pub fn new() -> Self {
WattpadClientBuilder::default().build()
}
pub fn builder() -> WattpadClientBuilder {
WattpadClientBuilder::default()
}
pub async fn authenticate(&self, username: &str, password: &str) -> Result<(), WattpadError> {
let url = "https://www.wattpad.com/auth/login?&_data=routes%2Fauth.login";
let mut payload = HashMap::new();
payload.insert("username", username);
payload.insert("password", password);
let response = self.http.post(url).form(&payload).send().await?;
#[cfg(not(target_arch = "wasm32"))]
{
if response.cookies().next().is_none() {
self.is_authenticated.store(false, Ordering::SeqCst);
return Err(WattpadError::AuthenticationFailed);
}
}
#[cfg(target_arch = "wasm32")]
{
if !response.status().is_success() {
self.is_authenticated.store(false, Ordering::SeqCst);
return Err(WattpadError::AuthenticationFailed);
}
}
self.is_authenticated.store(true, Ordering::SeqCst);
Ok(())
}
pub async fn deauthenticate(&self) -> Result<(), WattpadError> {
let url = "https://www.wattpad.com/logout";
self.http.get(url).send().await?;
self.is_authenticated.store(false, Ordering::SeqCst);
Ok(())
}
pub fn is_authenticated(&self) -> bool {
self.is_authenticated.load(Ordering::SeqCst)
}
}
impl Default for WattpadClient {
fn default() -> Self {
Self::new()
}
}
async fn handle_response<T: serde::de::DeserializeOwned>(
response: reqwest::Response,
) -> Result<T, WattpadError> {
if response.status().is_success() {
let json = response.json::<T>().await?;
Ok(json)
} else {
let error_response = response.json::<ApiErrorResponse>().await?;
Err(error_response.into())
}
}
#[derive(Clone)] pub(crate) struct WattpadRequestBuilder<'a> {
client: &'a reqwest::Client,
is_authenticated: &'a Arc<AtomicBool>,
method: reqwest::Method,
path: String,
params: Vec<(&'static str, String)>,
auth_required: bool,
}
impl<'a> WattpadRequestBuilder<'a> {
pub(crate) fn new(
client: &'a reqwest::Client,
is_authenticated: &'a Arc<AtomicBool>,
method: reqwest::Method,
path: &str,
) -> Self {
Self {
client,
is_authenticated,
method,
path: path.to_string(),
params: Vec::new(),
auth_required: false,
}
}
fn check_endpoint_auth(&self) -> Result<(), WattpadError> {
if self.auth_required && !self.is_authenticated.load(Ordering::SeqCst) {
return Err(WattpadError::AuthenticationRequired {
field: "Endpoint".to_string(),
context: format!("The endpoint at '{}' requires authentication.", self.path),
});
}
Ok(())
}
pub(crate) fn requires_auth(mut self) -> Self {
self.auth_required = true;
self
}
pub(crate) fn maybe_param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
if let Some(val) = value {
self.params.push((key, val.to_string()));
}
self
}
pub(crate) fn fields<T>(mut self, fields: Option<&[T]>, wrap: Option<&str>) -> Result<Self, WattpadError>
where
T: ToString + DefaultableFields + AuthRequiredFields + PartialEq + Clone,
{
let fields_to_query = match fields {
Some(f) if !f.is_empty() => Cow::from(f),
_ => Cow::from(T::default_fields()),
};
if !self.is_authenticated.load(Ordering::SeqCst)
&& let Some(auth_field) = fields_to_query.iter().find(|f| f.auth_required()) {
return Err(WattpadError::AuthenticationRequired {
field: auth_field.to_string(),
context: format!(
"The field '{}' requires authentication.",
auth_field.to_string()
),
});
}
let fields_str = {
let base = fields_to_query
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(",");
wrap
.map(|w| format!("{w}({base})"))
.unwrap_or(base)
};
self.params.push(("fields", fields_str));
Ok(self)
}
pub(crate) fn param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
if let Some(val) = value {
self.params.push((key, val.to_string()));
}
self
}
pub(crate) async fn execute<T: serde::de::DeserializeOwned>(self) -> Result<T, WattpadError> {
self.check_endpoint_auth()?;
let url = format!("https://www.wattpad.com{}", self.path);
let response = self
.client
.request(self.method, &url)
.query(&self.params)
.send()
.await?;
handle_response(response).await
}
pub(crate) async fn execute_raw_text(self) -> Result<String, WattpadError> {
self.check_endpoint_auth()?;
let url = format!("https://www.wattpad.com{}", self.path);
let response = self
.client
.request(self.method, &url)
.query(&self.params)
.send()
.await?;
if response.status().is_success() {
Ok(response.text().await?)
} else {
let error_response = response.json::<ApiErrorResponse>().await?;
Err(error_response.into())
}
}
pub(crate) async fn execute_bytes(self) -> Result<Bytes, WattpadError> {
self.check_endpoint_auth()?;
let url = format!("https://www.wattpad.com{}", self.path);
let response = self
.client
.request(self.method, &url)
.query(&self.params)
.send()
.await?;
if response.status().is_success() {
Ok(response.bytes().await?)
} else {
let error_response = response.json::<ApiErrorResponse>().await?;
Err(error_response.into())
}
}
}