use std::{num::NonZeroU32, sync::Arc, time::Duration};
use governor::{DefaultDirectRateLimiter, Quota};
use http::header::CONTENT_TYPE;
use reqwest::{
self, Body, Method, Url,
header::{ACCEPT_LANGUAGE, HeaderValue},
};
use crate::{config::Config, error::Result};
pub struct Client {
pub unlimited: reqwest::Client,
rate_limiter: DefaultDirectRateLimiter,
pub cookie_jar: Option<Arc<reqwest_cookie_store::CookieStoreMutex>>,
}
impl Client {
const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(5);
const RATE_LIMIT_CALLS_PER_INTERVAL: u8 = 50;
const KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(60);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
const READ_TIMEOUT: Duration = Duration::from_secs(5);
const CONTENT_TYPE_TEXT: HeaderValue = HeaderValue::from_static("text/plain;charset=UTF-8");
const CONTENT_TYPE_JSON: HeaderValue = HeaderValue::from_static("application/json");
pub fn new(
config: &Config,
cookie_jar: Option<reqwest_cookie_store::CookieStore>,
) -> Result<Self> {
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(lang) = HeaderValue::from_str(&config.app_lang) {
headers.insert(ACCEPT_LANGUAGE, lang);
}
let cookie_jar =
cookie_jar.map(|jar| Arc::new(reqwest_cookie_store::CookieStoreMutex::new(jar)));
let mut http_client = reqwest::Client::builder()
.tcp_keepalive(Self::KEEPALIVE_TIMEOUT)
.connect_timeout(Self::CONNECT_TIMEOUT)
.read_timeout(Self::READ_TIMEOUT)
.default_headers(headers)
.user_agent(&config.user_agent)
.local_address(config.bind_address);
if let Some(ref jar) = cookie_jar {
http_client = http_client.cookie_provider(Arc::clone(jar));
}
let replenish_interval =
Self::RATE_LIMIT_INTERVAL / u32::from(Self::RATE_LIMIT_CALLS_PER_INTERVAL);
let quota = Quota::with_period(replenish_interval)
.expect("quota time interval is zero")
.allow_burst(
NonZeroU32::new(Self::RATE_LIMIT_CALLS_PER_INTERVAL.into())
.expect("calls per interval is zero"),
);
Ok(Self {
unlimited: http_client.build()?,
rate_limiter: governor::RateLimiter::direct(quota),
cookie_jar,
})
}
pub fn with_cookies(
config: &Config,
cookie_jar: reqwest_cookie_store::CookieStore,
) -> Result<Self> {
Self::new(config, Some(cookie_jar))
}
pub fn without_cookies(config: &Config) -> Result<Self> {
Self::new(config, None)
}
#[inline]
pub fn request<U, T>(&self, method: Method, url: U, body: T) -> reqwest::Request
where
U: Into<Url>,
T: Into<Body>,
{
let mut request = reqwest::Request::new(method, url.into());
let body_mut = request.body_mut();
*body_mut = Some(body.into());
request
}
#[inline]
pub fn text<U, T>(&self, url: U, body: T) -> reqwest::Request
where
U: Into<Url>,
T: Into<Body>,
{
let mut request = self.request(Method::POST, url, body);
request
.headers_mut()
.insert(CONTENT_TYPE, Self::CONTENT_TYPE_TEXT);
request
}
#[inline]
pub fn json<U, T>(&self, url: U, body: T) -> reqwest::Request
where
U: Into<Url>,
T: Into<Body>,
{
let mut request = self.request(Method::POST, url, body);
request
.headers_mut()
.insert(CONTENT_TYPE, Self::CONTENT_TYPE_JSON);
request
}
#[inline]
pub fn get<U, T>(&self, url: U, body: T) -> reqwest::Request
where
U: Into<Url>,
T: Into<Body>,
{
self.request(Method::GET, url, body)
}
pub async fn execute(&self, request: reqwest::Request) -> Result<reqwest::Response> {
self.rate_limiter.until_ready().await;
match self.unlimited.execute(request).await {
Ok(response) => response.error_for_status().map_err(Into::into),
Err(e) => Err(e.into()),
}
}
}