mod auth;
mod builder;
pub use builder::*;
use std::marker::PhantomData;
use std::sync::Arc;
use crate::error::{Error, Result};
use crate::internal::ApiRateLimiter;
use crate::models::{Credentials, Language, Platform};
mod private {
pub trait Sealed {}
impl Sealed for super::Unauthenticated {}
impl Sealed for super::Authenticated {}
}
pub trait AuthState: private::Sealed {}
#[derive(Debug, Clone, Copy)]
pub struct Unauthenticated;
impl AuthState for Unauthenticated {}
#[derive(Debug, Clone, Copy)]
pub struct Authenticated;
impl AuthState for Authenticated {}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub platform: Platform,
pub language: Language,
pub crossplay: bool,
pub rate_limit: u32,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
platform: Platform::Pc,
language: Language::English,
crossplay: true,
rate_limit: 3,
}
}
}
pub struct Client<S: AuthState = Unauthenticated> {
pub(crate) http: reqwest::Client,
pub(crate) config: ClientConfig,
pub(crate) limiter: Arc<ApiRateLimiter>,
pub(crate) credentials: Option<Credentials>,
pub(crate) _state: PhantomData<S>,
}
impl Client<Unauthenticated> {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub async fn from_credentials(credentials: Credentials) -> Result<Client<Authenticated>> {
Self::builder().build()?.login(credentials).await
}
pub async fn from_credentials_with_config(
credentials: Credentials,
config: ClientConfig,
) -> Result<Client<Authenticated>> {
Self::builder()
.config(config)
.build()?
.login(credentials)
.await
}
pub async fn validate_credentials(credentials: &Credentials) -> Result<bool> {
use crate::internal::{BASE_URL, build_authenticated_client};
if let Some(token) = credentials.token() {
let http = build_authenticated_client(Platform::Pc, Language::English, true, token)
.map_err(Error::Network)?;
match http.get(format!("{}/me", BASE_URL)).send().await {
Ok(resp) if resp.status().is_success() => Ok(true),
Ok(resp) if resp.status() == reqwest::StatusCode::UNAUTHORIZED => Ok(false),
Ok(resp) => Err(Error::api(
resp.status(),
format!("Unexpected response: {}", resp.status()),
)),
Err(e) => Err(Error::Network(e)),
}
} else {
Ok(true)
}
}
pub(crate) fn new_unauthenticated(
http: reqwest::Client,
config: ClientConfig,
limiter: Arc<ApiRateLimiter>,
) -> Self {
Self {
http,
config,
limiter,
credentials: None,
_state: PhantomData,
}
}
}
impl<S: AuthState> Client<S> {
pub fn config(&self) -> &ClientConfig {
&self.config
}
pub fn platform(&self) -> Platform {
self.config.platform
}
pub fn language(&self) -> Language {
self.config.language
}
pub(crate) async fn wait_for_rate_limit(&self) {
self.limiter.until_ready().await;
}
}
impl Client<Authenticated> {
pub fn credentials(&self) -> &Credentials {
self.credentials
.as_ref()
.expect("Authenticated client must have credentials")
}
pub fn export_session(&self) -> Credentials {
self.credentials().clone()
}
pub fn token(&self) -> &str {
self.credentials()
.token()
.expect("Authenticated client must have token")
}
pub fn device_id(&self) -> &str {
&self.credentials().device_id
}
pub(crate) fn new_authenticated(
http: reqwest::Client,
config: ClientConfig,
limiter: Arc<ApiRateLimiter>,
credentials: Credentials,
) -> Self {
Self {
http,
config,
limiter,
credentials: Some(credentials),
_state: PhantomData,
}
}
}
impl Clone for Client<Unauthenticated> {
fn clone(&self) -> Self {
Self {
http: self.http.clone(),
config: self.config.clone(),
limiter: self.limiter.clone(),
credentials: None,
_state: PhantomData,
}
}
}
impl Clone for Client<Authenticated> {
fn clone(&self) -> Self {
Self {
http: self.http.clone(),
config: self.config.clone(),
limiter: self.limiter.clone(),
credentials: self.credentials.clone(),
_state: PhantomData,
}
}
}