use std::sync::Arc;
use reqwest::header::{HeaderValue, IF_MODIFIED_SINCE, LAST_MODIFIED, USER_AGENT};
use reqwest::StatusCode;
use serde::de::DeserializeOwned;
use crate::archive::Archive;
use crate::board::{Board, BoardList};
use crate::catalog::Catalog;
use crate::error::{Error, Result};
use crate::index::IndexPage;
use crate::thread::Thread;
use crate::threadlist::ThreadList;
pub const API_HOST: &str = "https://a.4cdn.org";
pub const CDN_HOST: &str = "https://i.4cdn.org";
const USER_AGENT_STR: &str = concat!("chan-rs/", env!("CARGO_PKG_VERSION"));
#[derive(Clone)]
pub struct Client {
http: reqwest::Client,
inner: Arc<Inner>,
}
struct Inner {
api_host: String,
}
#[derive(Debug, Clone)]
pub struct Conditional<T> {
pub value: T,
pub last_modified: Option<String>,
}
impl Client {
pub fn new() -> Self {
let http = reqwest::Client::builder()
.user_agent(USER_AGENT_STR)
.build()
.expect("reqwest::Client::builder default config is valid");
Self::with_client(http)
}
pub fn with_client(http: reqwest::Client) -> Self {
Self {
http,
inner: Arc::new(Inner {
api_host: API_HOST.to_string(),
}),
}
}
pub fn with_api_host(mut self, host: impl Into<String>) -> Self {
self.inner = Arc::new(Inner { api_host: host.into() });
self
}
pub async fn get_boards(&self) -> Result<Vec<Board>> {
let list: BoardList = self.get_json("/boards.json").await?;
Ok(list.boards)
}
pub async fn get_board_catalog(&self, board: &str) -> Result<Catalog> {
self.get_json(&format!("/{}/catalog.json", board)).await
}
pub async fn get_threads(&self, board: &str) -> Result<ThreadList> {
self.get_json(&format!("/{}/threads.json", board)).await
}
pub async fn get_archive(&self, board: &str) -> Result<Archive> {
self.get_json(&format!("/{}/archive.json", board)).await
}
pub async fn get_index_page(&self, board: &str, page: u8) -> Result<IndexPage> {
self.get_json(&format!("/{}/{}.json", board, page)).await
}
pub async fn get_full_thread(&self, board: &str, thread_no: u64) -> Result<Thread> {
self.get_json(&format!("/{}/thread/{}.json", board, thread_no))
.await
}
pub async fn get_threads_if_modified(
&self,
board: &str,
since: Option<&str>,
) -> Result<Option<Conditional<ThreadList>>> {
self.get_json_if_modified(&format!("/{}/threads.json", board), since)
.await
}
pub async fn get_catalog_if_modified(
&self,
board: &str,
since: Option<&str>,
) -> Result<Option<Conditional<Catalog>>> {
self.get_json_if_modified(&format!("/{}/catalog.json", board), since)
.await
}
pub async fn get_full_thread_if_modified(
&self,
board: &str,
thread_no: u64,
since: Option<&str>,
) -> Result<Option<Conditional<Thread>>> {
self.get_json_if_modified(
&format!("/{}/thread/{}.json", board, thread_no),
since,
)
.await
}
async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = format!("{}{}", self.inner.api_host, path);
let resp = self
.http
.get(&url)
.header(USER_AGENT, USER_AGENT_STR)
.send()
.await?;
Self::check_status(&resp, &url)?;
let bytes = resp.bytes().await?;
let value = serde_json::from_slice(&bytes)?;
Ok(value)
}
async fn get_json_if_modified<T: DeserializeOwned>(
&self,
path: &str,
since: Option<&str>,
) -> Result<Option<Conditional<T>>> {
let url = format!("{}{}", self.inner.api_host, path);
let mut req = self.http.get(&url);
if let Some(s) = since {
let header_value = HeaderValue::from_str(s)
.map_err(|e| Error::InvalidHeader(e.to_string()))?;
req = req.header(IF_MODIFIED_SINCE, header_value);
}
req = req.header(USER_AGENT, USER_AGENT_STR);
let resp = req.send().await?;
if resp.status() == StatusCode::NOT_MODIFIED {
return Ok(None);
}
Self::check_status(&resp, &url)?;
let last_modified = resp
.headers()
.get(LAST_MODIFIED)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let bytes = resp.bytes().await?;
let value = serde_json::from_slice(&bytes)?;
Ok(Some(Conditional { value, last_modified }))
}
fn check_status(resp: &reqwest::Response, url: &str) -> Result<()> {
let status = resp.status();
if status == StatusCode::NOT_FOUND {
return Err(Error::NotFound(url.to_string()));
}
if !status.is_success() {
return Err(Error::Status {
status: status.as_u16(),
url: url.to_string(),
});
}
Ok(())
}
}
impl Default for Client {
fn default() -> Self { Self::new() }
}