use eyre::{Result, WrapErr};
use kuchiki::traits::*;
use serde::de::DeserializeOwned;
use std::{io::Read, thread, time::Duration};
use url::Url;
const REFERER: &str = "https://piccoma.com/fr";
const USER_AGENT: &str =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0";
#[derive(Clone)]
pub struct Client {
agent: ureq::Agent,
delay: Duration,
retry: u8,
}
impl Client {
pub fn new(retry: u8) -> Self {
Self {
agent: ureq::builder().user_agent(USER_AGENT).build(),
delay: Duration::from_secs(1),
retry,
}
}
pub fn is_logged_in(&self) -> bool {
self.agent
.cookie_store()
.contains("piccoma.com", "/", "access_token")
}
pub fn login(&self, email: &str, password: &str) -> Result<()> {
let request = self
.agent
.request("POST", "https://piccoma.com/fr/api/auth/signin")
.set("accept", "text/html");
request
.send_json(ureq::json!({
"email": email,
"password": password,
"redirect": REFERER,
}))
.context("login")?;
Ok(())
}
pub fn get_html(&self, url: &Url) -> Result<kuchiki::NodeRef> {
let request = self
.agent
.request_url("GET", url)
.set("accept", "text/html");
let response = self.call(request).context("get HTML")?;
let html = response.into_string().context("read HTML")?;
Ok(kuchiki::parse_html().one(html))
}
pub fn get_json<T>(&self, url: &Url) -> Result<T>
where
T: DeserializeOwned,
{
let request = self
.agent
.request_url("GET", url)
.set("accept", "application/json");
let response = self.call(request).context("get JSON")?;
serde_json::from_reader(response.into_reader()).context("read JSON")
}
pub fn get_image(&self, url: &Url, buf: &mut Vec<u8>) -> Result<()> {
let request =
self.agent.request_url("GET", url).set("accept", "image/*");
let response = self.call(request).context("get image")?;
response
.into_reader()
.read_to_end(buf)
.context("read image")?;
Ok(())
}
fn call(&self, request: ureq::Request) -> Result<ureq::Response> {
thread::sleep(self.delay);
let request = request.set("Referer", REFERER);
let mut i = 0;
loop {
i += 1;
let res = request.clone().call();
if let Err(ureq::Error::Status(code, ref response)) = res {
if is_request_retryable(code) && i <= self.retry {
let delay = self.retry_delay(response);
thread::sleep(delay);
continue;
}
}
return res.context("HTTP request failed");
}
}
fn retry_delay(&self, response: &ureq::Response) -> Duration {
response
.header("retry-after")
.and_then(|h| h.parse::<u64>().ok())
.map_or(self.delay, Duration::from_secs)
}
}
fn is_request_retryable(http_status: u16) -> bool {
(500..=599).contains(&http_status) || http_status == 429
}