1use crate::endpoints::story::StoryClient;
8use crate::error::{ApiErrorResponse, InkittError};
9use crate::types::LoginResponse;
10use bytes::Bytes;
11use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
12use reqwest::Client as ReqwestClient;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, RwLock};
15#[derive(Default)]
21pub struct InkittClientBuilder {
22 client: Option<ReqwestClient>,
23 user_agent: Option<String>,
24 headers: Option<HeaderMap>,
25}
26
27impl InkittClientBuilder {
28 pub fn reqwest_client(mut self, client: ReqwestClient) -> Self {
32 self.client = Some(client);
33 self
34 }
35
36 pub fn user_agent(mut self, user_agent: &str) -> Self {
38 self.user_agent = Some(user_agent.to_string());
39 self
40 }
41
42 pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
44 self.headers
45 .get_or_insert_with(HeaderMap::new)
46 .insert(key, value);
47 self
48 }
49
50 pub fn build(self) -> InkittClient {
54 let http_client = match self.client {
55 Some(client) => client,
57 None => {
59 let mut headers = self.headers.unwrap_or_default();
60
61 let ua_string = self.user_agent.unwrap_or_else(||
63 "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()
64 );
65
66 headers.insert(
68 USER_AGENT,
69 HeaderValue::from_str(&ua_string).expect("Invalid User-Agent string"),
70 );
71
72 let mut client_builder = ReqwestClient::builder().default_headers(headers);
73
74 #[cfg(not(target_arch = "wasm32"))]
75 {
76 client_builder = client_builder.cookie_store(true);
77 }
78
79 client_builder
80 .build()
81 .expect("Failed to build reqwest client")
82 }
83 };
84
85 let auth_flag = Arc::new(AtomicBool::new(false));
86 let auth_token = Arc::new(RwLock::new(None));
87
88 InkittClient {
89 story: StoryClient {
90 http: http_client.clone(),
91 is_authenticated: auth_flag.clone(),
92 auth_token: auth_token.clone(),
93 },
94 http: http_client,
95 is_authenticated: auth_flag,
96 auth_token,
97 }
98 }
99}
100
101pub struct InkittClient {
106 http: reqwest::Client,
108 is_authenticated: Arc<AtomicBool>,
110 auth_token: Arc<RwLock<Option<String>>>,
112
113 pub story: StoryClient,
115}
116
117impl InkittClient {
118 pub fn new() -> Self {
122 InkittClientBuilder::default().build()
123 }
124
125 pub fn builder() -> InkittClientBuilder {
129 InkittClientBuilder::default()
130 }
131
132 pub async fn authenticate(
138 &self,
139 email: &str,
140 password: &str,
141 ) -> Result<LoginResponse, InkittError> {
142 let url = "https://harry.inkitt.com/2/current_user/login_or_signup";
143
144 let payload = serde_json::json!({
145 "session_params": {
146 "email": email,
147 "password": password
148 }
149 });
150
151 let response = self.http.post(url).json(&payload).send().await?;
152
153 if !response.status().is_success() {
154 self.is_authenticated.store(false, Ordering::SeqCst);
155 let mut token_lock = self.auth_token.write().unwrap();
157 *token_lock = None;
158 return Err(InkittError::AuthenticationFailed);
159 }
160
161 let login_data: LoginResponse = response
163 .json()
164 .await
165 .map_err(|_| InkittError::AuthenticationFailed)?;
166
167 {
169 let mut token_lock = self.auth_token.write().unwrap();
170 *token_lock = Some(login_data.response.secret_token.clone());
171 }
172
173 self.is_authenticated.store(true, Ordering::SeqCst);
175
176 Ok(login_data)
178 }
179
180 pub async fn deauthenticate(&self) -> Result<(), InkittError> {
191 let url = "https://www.Inkitt.com/logout";
192
193 self.http.get(url).send().await?;
196
197 self.is_authenticated.store(false, Ordering::SeqCst);
199 Ok(())
200 }
201
202 pub fn is_authenticated(&self) -> bool {
207 self.is_authenticated.load(Ordering::SeqCst)
208 }
209}
210
211impl Default for InkittClient {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220async fn handle_response<T: serde::de::DeserializeOwned>(
225 response: reqwest::Response,
226) -> Result<T, InkittError> {
227 if response.status().is_success() {
228 let json = response.json::<T>().await?;
229 Ok(json)
230 } else {
231 let error_response = response.json::<ApiErrorResponse>().await?;
232 Err(error_response.into())
233 }
234}
235
236pub(crate) struct InkittRequestBuilder<'a> {
243 client: &'a reqwest::Client,
244 is_authenticated: &'a Arc<AtomicBool>,
245 auth_token: &'a Arc<RwLock<Option<String>>>,
246 method: reqwest::Method,
247 path: String,
248 params: Vec<(&'static str, String)>,
249 auth_required: bool,
250}
251
252impl<'a> InkittRequestBuilder<'a> {
253 pub(crate) fn new(
255 client: &'a reqwest::Client,
256 is_authenticated: &'a Arc<AtomicBool>,
257 auth_token: &'a Arc<RwLock<Option<String>>>,
258 method: reqwest::Method,
259 path: &str,
260 ) -> Self {
261 Self {
262 client,
263 is_authenticated,
264 auth_token,
265 method,
266 path: path.to_string(),
267 params: Vec::new(),
268 auth_required: false,
269 }
270 }
271
272 fn check_endpoint_auth(&self) -> Result<(), InkittError> {
274 if self.auth_required && !self.is_authenticated.load(Ordering::SeqCst) {
275 return Err(InkittError::AuthenticationRequired {
276 field: "Endpoint".to_string(),
277 context: format!("The endpoint at '{}' requires authentication.", self.path),
278 });
279 }
280 Ok(())
281 }
282
283 pub(crate) fn requires_auth(mut self) -> Self {
287 self.auth_required = true;
288 self
289 }
290
291 pub(crate) fn maybe_param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
295 if let Some(val) = value {
296 self.params.push((key, val.to_string()));
297 }
298 self
299 }
300
301 pub(crate) fn param<T: ToString>(mut self, key: &'static str, value: Option<T>) -> Self {
303 if let Some(val) = value {
304 self.params.push((key, val.to_string()));
305 }
306 self
307 }
308
309 pub(crate) async fn execute<T: serde::de::DeserializeOwned>(self) -> Result<T, InkittError> {
311 self.check_endpoint_auth()?;
312
313 let url = format!("https://harry.inkitt.com{}", self.path);
314
315 let mut request_builder = self.client.request(self.method, &url).query(&self.params);
317
318 if let Ok(lock) = self.auth_token.read()
320 && let Some(token) = &*lock {
321 request_builder =
323 request_builder.header("Authorization", format!("Bearer {}", token));
324 }
325
326
327 let response = request_builder.send().await?;
328 handle_response(response).await
329 }
330
331 pub(crate) async fn execute_raw_text(self) -> Result<String, InkittError> {
333 self.check_endpoint_auth()?;
334
335 let url = format!("https://harry.inkitt.com{}", self.path);
336 let response = self
337 .client
338 .request(self.method, &url)
339 .query(&self.params)
340 .send()
341 .await?;
342
343 if response.status().is_success() {
344 Ok(response.text().await?)
345 } else {
346 let error_response = response.json::<ApiErrorResponse>().await?;
347 Err(error_response.into())
348 }
349 }
350
351 pub(crate) async fn execute_bytes(self) -> Result<Bytes, InkittError> {
355 self.check_endpoint_auth()?;
356
357 let url = format!("https://harry.inkitt.com{}", self.path);
358 let response = self
359 .client
360 .request(self.method, &url)
361 .query(&self.params)
362 .send()
363 .await?;
364
365 if response.status().is_success() {
366 Ok(response.bytes().await?)
367 } else {
368 let error_response = response.json::<ApiErrorResponse>().await?;
369 Err(error_response.into())
370 }
371 }
372}