1use crate::error::{ApiErrorResponse, WattpadError};
8use crate::endpoints::story::StoryClient;
9use crate::endpoints::user::UserClient;
10use crate::field::{AuthRequiredFields, DefaultableFields};
11use bytes::Bytes;
12use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::Arc;
17
18pub struct WattpadClient {
23 http: reqwest::Client,
25 is_authenticated: Arc<AtomicBool>,
27 pub user: UserClient,
29 pub story: StoryClient,
31}
32
33impl WattpadClient {
34 pub fn new() -> Self {
40 let auth_flag = Arc::new(AtomicBool::new(false));
41
42 let mut headers = HeaderMap::new();
43 headers.insert(
44 USER_AGENT,
45 HeaderValue::from_static(
46 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
47 ),
48 );
49
50 let http_client = reqwest::Client::builder()
51 .default_headers(headers)
52 .cookie_store(true) .build()
54 .expect("Failed to build reqwest client");
55
56 WattpadClient {
57 user: UserClient {
58 http: http_client.clone(),
59 is_authenticated: auth_flag.clone(),
60 },
61 story: StoryClient {
62 http: http_client.clone(),
63 is_authenticated: auth_flag.clone(),
64 },
65 http: http_client,
66 is_authenticated: auth_flag,
67 }
68 }
69
70 pub async fn authenticate(&self, username: &str, password: &str) -> Result<(), WattpadError> {
85 let url = "https://www.wattpad.com/auth/login?&_data=routes%2Fauth.login";
86
87 let mut payload = HashMap::new();
88 payload.insert("username", username);
89 payload.insert("password", password);
90
91 let response = self.http.post(url).form(&payload).send().await?;
92
93 if response.cookies().next().is_none() {
95 self.is_authenticated.store(false, Ordering::SeqCst);
96 return Err(WattpadError::AuthenticationFailed);
97 }
98
99 self.is_authenticated.store(true, Ordering::SeqCst);
100 Ok(())
101 }
102
103 pub fn is_authenticated(&self) -> bool {
108 self.is_authenticated.load(Ordering::SeqCst)
109 }
110}
111
112impl Default for WattpadClient {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121async fn handle_response<T: serde::de::DeserializeOwned>(
126 response: reqwest::Response,
127) -> Result<T, WattpadError> {
128 if response.status().is_success() {
129 let json = response.json::<T>().await?;
130 Ok(json)
131 } else {
132 let error_response = response.json::<ApiErrorResponse>().await?;
133 Err(error_response.into())
134 }
135}
136
137pub(crate) struct WattpadRequestBuilder<'a> {
144 client: &'a reqwest::Client,
145 is_authenticated: &'a Arc<AtomicBool>,
146 method: reqwest::Method,
147 path: String,
148 params: Vec<(&'static str, String)>,
149 auth_required: bool,
150}
151
152impl<'a> WattpadRequestBuilder<'a> {
153 pub(crate) fn new(
155 client: &'a reqwest::Client,
156 is_authenticated: &'a Arc<AtomicBool>,
157 method: reqwest::Method,
158 path: &str,
159 ) -> Self {
160 Self {
161 client,
162 is_authenticated,
163 method,
164 path: path.to_string(),
165 params: Vec::new(),
166 auth_required: false,
167 }
168 }
169
170 fn check_endpoint_auth(&self) -> Result<(), WattpadError> {
172 if self.auth_required && !self.is_authenticated.load(Ordering::SeqCst) {
173 return Err(WattpadError::AuthenticationRequired {
174 field: "Endpoint".to_string(),
175 context: format!("The endpoint at '{}' requires authentication.", self.path),
176 });
177 }
178 Ok(())
179 }
180
181 pub(crate) fn requires_auth(mut self) -> Self {
185 self.auth_required = true;
186 self
187 }
188
189 pub(crate) fn maybe_param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
193 if let Some(val) = value {
194 self.params.push((key, val.to_string()));
195 }
196 self
197 }
198
199 pub(crate) fn fields<T>(mut self, fields: Option<&[T]>) -> Result<Self, WattpadError>
209 where
210 T: ToString + DefaultableFields + AuthRequiredFields + PartialEq + Clone,
211 {
212 let fields_to_query = match fields {
213 Some(f) if !f.is_empty() => Cow::from(f),
214 _ => Cow::from(T::default_fields()),
215 };
216
217 if !self.is_authenticated.load(Ordering::SeqCst) {
218 if let Some(auth_field) = fields_to_query.iter().find(|f| f.auth_required()) {
219 return Err(WattpadError::AuthenticationRequired {
220 field: auth_field.to_string(),
221 context: format!(
222 "The field '{}' requires authentication.",
223 auth_field.to_string()
224 ),
225 });
226 }
227 }
228
229 let fields_str = fields_to_query
230 .iter()
231 .map(|f| f.to_string())
232 .collect::<Vec<_>>()
233 .join(",");
234
235 self.params.push(("fields", fields_str));
236 Ok(self)
237 }
238
239 pub(crate) fn param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
241 if let Some(val) = value {
242 self.params.push((key, val.to_string()));
243 }
244 self
245 }
246
247 pub(crate) async fn execute<T: serde::de::DeserializeOwned>(self) -> Result<T, WattpadError> {
249 self.check_endpoint_auth()?;
250
251 let url = format!("https://www.wattpad.com{}", self.path);
252 let response = self
253 .client
254 .request(self.method, &url)
255 .query(&self.params)
256 .send()
257 .await?;
258 handle_response(response).await
259 }
260
261 pub(crate) async fn execute_raw_text(self) -> Result<String, WattpadError> {
263 self.check_endpoint_auth()?;
264
265 let url = format!("https://www.wattpad.com{}", self.path);
266 let response = self
267 .client
268 .request(self.method, &url)
269 .query(&self.params)
270 .send()
271 .await?;
272
273 if response.status().is_success() {
274 Ok(response.text().await?)
275 } else {
276 let error_response = response.json::<ApiErrorResponse>().await?;
277 Err(error_response.into())
278 }
279 }
280
281 pub(crate) async fn execute_bytes(self) -> Result<Bytes, WattpadError> {
285 self.check_endpoint_auth()?;
286
287 let url = format!("https://www.wattpad.com{}", self.path);
288 let response = self
289 .client
290 .request(self.method, &url)
291 .query(&self.params)
292 .send()
293 .await?;
294
295 if response.status().is_success() {
296 Ok(response.bytes().await?)
297 } else {
298 let error_response = response.json::<ApiErrorResponse>().await?;
299 Err(error_response.into())
300 }
301 }
302}