use std::ops::Add;
use std::sync::Mutex;
use std::time::Duration;
use http::{HeaderMap, StatusCode};
use serde::Deserialize;
use time::OffsetDateTime;
use crate::auth::AccessTokenResponse;
use crate::classic::WorldOfWarcraftClassicConnector;
use crate::connectors::ClientConnector;
use crate::errors::{BubbleHearthError, BubbleHearthResult};
use crate::hearthstone::HearthstoneConnector;
use crate::localization::Locale;
use crate::regionality::AccountRegion;
const DEFAULT_TIMEOUT_SECONDS: u8 = 5;
#[derive(Debug)]
pub struct BubbleHearthClient {
http: reqwest::Client,
pub(crate) region: AccountRegion,
pub(crate) locale: Locale,
client_id: String,
client_secret: String,
access_token: Mutex<Option<String>>,
expires_at: Mutex<OffsetDateTime>,
}
impl BubbleHearthClient {
pub fn new(
client_id: String,
client_secret: String,
region: AccountRegion,
locale: Locale,
) -> Self {
let default_timeout = Duration::from_secs(DEFAULT_TIMEOUT_SECONDS.into());
Self::new_with_timeout(client_id, client_secret, region, locale, default_timeout)
}
pub fn new_with_timeout(
client_id: String,
client_secret: String,
region: AccountRegion,
locale: Locale,
timeout: Duration,
) -> Self {
let client = reqwest::ClientBuilder::new()
.timeout(timeout)
.build()
.unwrap();
Self {
http: client,
client_id,
client_secret,
region,
locale,
access_token: Mutex::new(None),
expires_at: Mutex::new(OffsetDateTime::UNIX_EPOCH),
}
}
fn try_access_token(&self) -> BubbleHearthResult<Option<String>> {
match self.access_token.try_lock() {
Ok(token_lock) => match token_lock.as_ref() {
None => Err(BubbleHearthError::AccessTokenNotFound),
Some(token) => match self.try_refresh_required() {
Ok(refresh_required) => {
if refresh_required {
Ok(None)
} else {
Ok(Some(token.to_owned()))
}
}
Err(e) => Err(BubbleHearthError::AuthenticationLockFailed(e.to_string())),
},
},
Err(e) => Err(BubbleHearthError::AuthenticationLockFailed(e.to_string())),
}
}
fn try_refresh_required(&self) -> BubbleHearthResult<bool> {
match self.expires_at.try_lock() {
Ok(expiration) => {
let now = OffsetDateTime::now_utc();
Ok(expiration.le(&now))
}
Err(e) => Err(BubbleHearthError::AuthenticationLockFailed(e.to_string())),
}
}
pub async fn get_access_token(&self) -> BubbleHearthResult<String> {
if let Ok(Some(cached_token)) = self.try_access_token() {
return Ok(cached_token);
}
let form = reqwest::multipart::Form::new().text("grant_type", "client_credentials");
let token_response = self
.http
.post(self.region.get_token_endpoint())
.multipart(form)
.basic_auth(&self.client_id, Some(&self.client_secret))
.send()
.await?
.json::<AccessTokenResponse>()
.await?;
let access_token = token_response.access_token;
if let Ok(mut token_lock) = self.access_token.try_lock() {
*token_lock = Some(access_token.clone());
}
if let Ok(mut expiration_lock) = self.expires_at.try_lock() {
let expires_in_duration = Duration::from_secs(token_response.expires_in);
*expiration_lock = OffsetDateTime::now_utc().add(expires_in_duration);
}
Ok(access_token)
}
fn get_namespace_locality(&self) -> String {
format!("dynamic-classic-{}", self.region.get_region_abbreviation())
}
async fn send_request(&self, url: String) -> BubbleHearthResult<reqwest::Response> {
let token = self.get_access_token().await?;
let mut headers = HeaderMap::new();
headers.append(
"Battlenet-Namespace",
self.get_namespace_locality().parse().unwrap(),
);
let response = self
.http
.get(url)
.headers(headers)
.bearer_auth(token)
.send()
.await?;
Ok(response)
}
pub(crate) async fn send_request_and_deserialize<T: for<'de> Deserialize<'de>>(
&self,
url: String,
) -> BubbleHearthResult<T> {
let response = self.send_request(url).await?.json::<T>().await?;
Ok(response)
}
pub(crate) async fn send_request_and_optionally_deserialize<T: for<'de> Deserialize<'de>>(
&self,
url: String,
) -> BubbleHearthResult<Option<T>> {
let response = self.send_request(url).await?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
let response = response.json::<T>().await?;
Ok(Some(response))
}
pub fn classic(&self) -> WorldOfWarcraftClassicConnector {
WorldOfWarcraftClassicConnector::new_connector(self)
}
pub fn hearthstone(&self) -> HearthstoneConnector {
HearthstoneConnector::new_connector(self)
}
}