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::Client as ReqwestClient; use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
14use std::borrow::Cow;
15use std::collections::HashMap;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::Arc;
18
19#[derive(Default)]
25pub struct WattpadClientBuilder {
26 client: Option<ReqwestClient>,
27 user_agent: Option<String>,
28 headers: Option<HeaderMap>,
29}
30
31impl WattpadClientBuilder {
32 pub fn reqwest_client(mut self, client: ReqwestClient) -> Self {
36 self.client = Some(client);
37 self
38 }
39
40 pub fn user_agent(mut self, user_agent: &str) -> Self {
42 self.user_agent = Some(user_agent.to_string());
43 self
44 }
45
46 pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
48 self.headers.get_or_insert_with(HeaderMap::new).insert(key, value);
49 self
50 }
51
52 pub fn build(self) -> WattpadClient {
56 let http_client = match self.client {
57 Some(client) => client,
59 None => {
61 let mut headers = self.headers.unwrap_or_default();
62
63 let ua_string = self.user_agent.unwrap_or_else(||
65 "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()
66 );
67
68 headers.insert(reqwest::header::USER_AGENT, HeaderValue::from_str(&ua_string).expect("Invalid User-Agent string"));
70
71 ReqwestClient::builder()
72 .default_headers(headers)
73 .cookie_store(true)
74 .build()
75 .expect("Failed to build reqwest client")
76 }
77 };
78
79 let auth_flag = Arc::new(AtomicBool::new(false));
81 WattpadClient {
82 user: UserClient {
83 http: http_client.clone(),
84 is_authenticated: auth_flag.clone(),
85 },
86 story: StoryClient {
87 http: http_client.clone(),
88 is_authenticated: auth_flag.clone(),
89 },
90 http: http_client,
91 is_authenticated: auth_flag,
92 }
93 }
94}
95
96pub struct WattpadClient {
101 http: reqwest::Client,
103 is_authenticated: Arc<AtomicBool>,
105 pub user: UserClient,
107 pub story: StoryClient,
109}
110
111impl WattpadClient {
112 pub fn new() -> Self {
116 WattpadClientBuilder::default().build()
117 }
118
119 pub fn builder() -> WattpadClientBuilder {
123 WattpadClientBuilder::default()
124 }
125
126 pub async fn authenticate(&self, username: &str, password: &str) -> Result<(), WattpadError> {
141 let url = "https://www.wattpad.com/auth/login?&_data=routes%2Fauth.login";
142
143 let mut payload = HashMap::new();
144 payload.insert("username", username);
145 payload.insert("password", password);
146
147 let response = self.http.post(url).form(&payload).send().await?;
148
149 if response.cookies().next().is_none() {
151 self.is_authenticated.store(false, Ordering::SeqCst);
152 return Err(WattpadError::AuthenticationFailed);
153 }
154
155 self.is_authenticated.store(true, Ordering::SeqCst);
156 Ok(())
157 }
158
159 pub fn is_authenticated(&self) -> bool {
164 self.is_authenticated.load(Ordering::SeqCst)
165 }
166}
167
168impl Default for WattpadClient {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177async fn handle_response<T: serde::de::DeserializeOwned>(
182 response: reqwest::Response,
183) -> Result<T, WattpadError> {
184 if response.status().is_success() {
185 let json = response.json::<T>().await?;
186 Ok(json)
187 } else {
188 let error_response = response.json::<ApiErrorResponse>().await?;
189 Err(error_response.into())
190 }
191}
192
193pub(crate) struct WattpadRequestBuilder<'a> {
200 client: &'a reqwest::Client,
201 is_authenticated: &'a Arc<AtomicBool>,
202 method: reqwest::Method,
203 path: String,
204 params: Vec<(&'static str, String)>,
205 auth_required: bool,
206}
207
208impl<'a> WattpadRequestBuilder<'a> {
209 pub(crate) fn new(
211 client: &'a reqwest::Client,
212 is_authenticated: &'a Arc<AtomicBool>,
213 method: reqwest::Method,
214 path: &str,
215 ) -> Self {
216 Self {
217 client,
218 is_authenticated,
219 method,
220 path: path.to_string(),
221 params: Vec::new(),
222 auth_required: false,
223 }
224 }
225
226 fn check_endpoint_auth(&self) -> Result<(), WattpadError> {
228 if self.auth_required && !self.is_authenticated.load(Ordering::SeqCst) {
229 return Err(WattpadError::AuthenticationRequired {
230 field: "Endpoint".to_string(),
231 context: format!("The endpoint at '{}' requires authentication.", self.path),
232 });
233 }
234 Ok(())
235 }
236
237 pub(crate) fn requires_auth(mut self) -> Self {
241 self.auth_required = true;
242 self
243 }
244
245 pub(crate) fn maybe_param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
249 if let Some(val) = value {
250 self.params.push((key, val.to_string()));
251 }
252 self
253 }
254
255 pub(crate) fn fields<T>(mut self, fields: Option<&[T]>) -> Result<Self, WattpadError>
265 where
266 T: ToString + DefaultableFields + AuthRequiredFields + PartialEq + Clone,
267 {
268 let fields_to_query = match fields {
269 Some(f) if !f.is_empty() => Cow::from(f),
270 _ => Cow::from(T::default_fields()),
271 };
272
273 if !self.is_authenticated.load(Ordering::SeqCst) {
274 if let Some(auth_field) = fields_to_query.iter().find(|f| f.auth_required()) {
275 return Err(WattpadError::AuthenticationRequired {
276 field: auth_field.to_string(),
277 context: format!(
278 "The field '{}' requires authentication.",
279 auth_field.to_string()
280 ),
281 });
282 }
283 }
284
285 let fields_str = fields_to_query
286 .iter()
287 .map(|f| f.to_string())
288 .collect::<Vec<_>>()
289 .join(",");
290
291 self.params.push(("fields", fields_str));
292 Ok(self)
293 }
294
295 pub(crate) fn param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
297 if let Some(val) = value {
298 self.params.push((key, val.to_string()));
299 }
300 self
301 }
302
303 pub(crate) async fn execute<T: serde::de::DeserializeOwned>(self) -> Result<T, WattpadError> {
305 self.check_endpoint_auth()?;
306
307 let url = format!("https://www.wattpad.com{}", self.path);
308 let response = self
309 .client
310 .request(self.method, &url)
311 .query(&self.params)
312 .send()
313 .await?;
314 handle_response(response).await
315 }
316
317 pub(crate) async fn execute_raw_text(self) -> Result<String, WattpadError> {
319 self.check_endpoint_auth()?;
320
321 let url = format!("https://www.wattpad.com{}", self.path);
322 let response = self
323 .client
324 .request(self.method, &url)
325 .query(&self.params)
326 .send()
327 .await?;
328
329 if response.status().is_success() {
330 Ok(response.text().await?)
331 } else {
332 let error_response = response.json::<ApiErrorResponse>().await?;
333 Err(error_response.into())
334 }
335 }
336
337 pub(crate) async fn execute_bytes(self) -> Result<Bytes, WattpadError> {
341 self.check_endpoint_auth()?;
342
343 let url = format!("https://www.wattpad.com{}", self.path);
344 let response = self
345 .client
346 .request(self.method, &url)
347 .query(&self.params)
348 .send()
349 .await?;
350
351 if response.status().is_success() {
352 Ok(response.bytes().await?)
353 } else {
354 let error_response = response.json::<ApiErrorResponse>().await?;
355 Err(error_response.into())
356 }
357 }
358}