use crate::endpoints::story::StoryClient;
use crate::error::{ApiErrorResponse, InkittError};
use crate::types::LoginResponse;
use bytes::Bytes;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
use reqwest::Client as ReqwestClient;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
#[derive(Default)]
pub struct InkittClientBuilder {
client: Option<ReqwestClient>,
user_agent: Option<String>,
headers: Option<HeaderMap>,
}
impl InkittClientBuilder {
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) -> InkittClient {
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));
let auth_token = Arc::new(RwLock::new(None));
InkittClient {
story: StoryClient {
http: http_client.clone(),
is_authenticated: auth_flag.clone(),
auth_token: auth_token.clone(),
},
http: http_client,
is_authenticated: auth_flag,
auth_token,
}
}
}
pub struct InkittClient {
http: reqwest::Client,
is_authenticated: Arc<AtomicBool>,
auth_token: Arc<RwLock<Option<String>>>,
pub story: StoryClient,
}
impl InkittClient {
pub fn new() -> Self {
InkittClientBuilder::default().build()
}
pub fn builder() -> InkittClientBuilder {
InkittClientBuilder::default()
}
pub async fn authenticate(
&self,
email: &str,
password: &str,
) -> Result<LoginResponse, InkittError> {
let url = "https://harry.inkitt.com/2/current_user/login_or_signup";
let payload = serde_json::json!({
"session_params": {
"email": email,
"password": password
}
});
let response = self.http.post(url).json(&payload).send().await?;
if !response.status().is_success() {
self.is_authenticated.store(false, Ordering::SeqCst);
let mut token_lock = self.auth_token.write().unwrap();
*token_lock = None;
return Err(InkittError::AuthenticationFailed);
}
let login_data: LoginResponse = response
.json()
.await
.map_err(|_| InkittError::AuthenticationFailed)?;
{
let mut token_lock = self.auth_token.write().unwrap();
*token_lock = Some(login_data.response.secret_token.clone());
}
self.is_authenticated.store(true, Ordering::SeqCst);
Ok(login_data)
}
pub async fn deauthenticate(&self) -> Result<(), InkittError> {
let url = "https://www.Inkitt.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 InkittClient {
fn default() -> Self {
Self::new()
}
}
async fn handle_response<T: serde::de::DeserializeOwned>(
response: reqwest::Response,
) -> Result<T, InkittError> {
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())
}
}
pub(crate) struct InkittRequestBuilder<'a> {
client: &'a reqwest::Client,
is_authenticated: &'a Arc<AtomicBool>,
auth_token: &'a Arc<RwLock<Option<String>>>,
method: reqwest::Method,
path: String,
params: Vec<(&'static str, String)>,
auth_required: bool,
}
impl<'a> InkittRequestBuilder<'a> {
pub(crate) fn new(
client: &'a reqwest::Client,
is_authenticated: &'a Arc<AtomicBool>,
auth_token: &'a Arc<RwLock<Option<String>>>,
method: reqwest::Method,
path: &str,
) -> Self {
Self {
client,
is_authenticated,
auth_token,
method,
path: path.to_string(),
params: Vec::new(),
auth_required: false,
}
}
fn check_endpoint_auth(&self) -> Result<(), InkittError> {
if self.auth_required && !self.is_authenticated.load(Ordering::SeqCst) {
return Err(InkittError::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 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, InkittError> {
self.check_endpoint_auth()?;
let url = format!("https://harry.inkitt.com{}", self.path);
let mut request_builder = self.client.request(self.method, &url).query(&self.params);
if let Ok(lock) = self.auth_token.read()
&& let Some(token) = &*lock {
request_builder =
request_builder.header("Authorization", format!("Bearer {}", token));
}
let response = request_builder.send().await?;
handle_response(response).await
}
pub(crate) async fn execute_raw_text(self) -> Result<String, InkittError> {
self.check_endpoint_auth()?;
let url = format!("https://harry.inkitt.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, InkittError> {
self.check_endpoint_auth()?;
let url = format!("https://harry.inkitt.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())
}
}
}