initial git dump

This commit is contained in:
Jonas Rabenstein 2026-02-16 02:51:10 +01:00
commit 781e25470b
47 changed files with 2361 additions and 0 deletions

201
turnerbund/src/api/api.rs Normal file
View file

@ -0,0 +1,201 @@
use super::{
Request,
error,
token,
auth,
};
use crate::utils::Bool;
#[async_trait::async_trait]
trait Interface {
fn client(&self) -> &reqwest::Client;
fn url(&self, endpoint: &str) -> Result<reqwest::Url, url::ParseError>;
fn current_token(&self) -> Option<std::sync::Arc<token::Token>>;
async fn refresh_token(&self, api: Api) -> Result<Option<std::sync::Arc<token::Token>>, error::Token>;
}
#[derive(Debug)]
struct Internal<T: token::Provider> {
client: reqwest::Client,
base: reqwest::Url,
token_provider: T,
}
impl<T: token::Provider> Internal<T> {
fn new(client: reqwest::Client, base: reqwest::Url, token_provider: T)
-> std::sync::Arc<Self> {
std::sync::Arc::new(Self { client, base, token_provider })
}
#[tracing::instrument(skip_all, fields(endpoint=request.endpoint(), request, token))]
async fn call<R: Request + Sized>(&self, request: &R, token: Option<&token::Token>) -> Result<R::Response, error::Api>
{
let url = self.base.join(request.endpoint()).map_err(error::Api::Url)?;
let builder = self.client.request(R::METHOD, url);
let builder = if let Some(token) = token {
token.r#use(builder)
} else {
builder
};
let response = builder.json(request)
.send()
.await
.map_err(error::Api::Reqwest)?;
match response.status() {
reqwest::StatusCode::OK => (),
status => return Err(error::Api::Http(status)),
}
let result: R::Response = response.json().await?;
Ok(result)
}
}
async fn deserialize<R: Request>(response: reqwest::Response) -> Result<R::Response, error::Api>
{
let response: R::Response = response.json().await.map_err(error::Api::Reqwest)?;
Ok(response)
}
#[tracing::instrument(skip_all, fields(url, request, token))]
async fn call<R: Request>(client: &reqwest::Client, url: reqwest::Url, request: &R, token: Option<&token::Token>) -> Result<R::Response, error::Api> {
let builder = client.request(R::METHOD, url);
let builder = if let Some(token) = token {
builder.bearer_auth(token.as_ref())
} else {
builder
};
let response = builder.json(request)
.send()
.await
.map_err(error::Api::Reqwest)?;
match response.status() {
reqwest::StatusCode::OK =>
deserialize::<R>(response).await,
status => Err(error::Api::Http(status)),
}
}
#[async_trait::async_trait]
impl<T: token::Provider> Interface for Internal<T> {
fn client(&self) -> &reqwest::Client {
&self.client
}
fn url(&self, endpoint: &str) -> Result<reqwest::Url, url::ParseError> {
self.base.join(endpoint)
}
fn current_token(&self) -> Option<std::sync::Arc<token::Token>> {
self.token_provider.current()
}
async fn refresh_token(&self, api: Api) -> Result<Option<std::sync::Arc<token::Token>>, error::Token> {
self.token_provider.refresh(api).await
}
}
#[derive(Debug)]
struct NoToken;
#[async_trait::async_trait]
impl token::Provider for NoToken {
fn current(&self) -> Option<std::sync::Arc<token::Token>> {
None
}
async fn refresh(&self, _api: Api) -> Result<Option<std::sync::Arc<token::Token>>, error::Token> {
Ok(None)
}
}
#[derive(Clone)]
pub struct Api(std::sync::Arc<dyn Interface + Sync + Send>);
impl Api {
pub async fn new<A: auth::Provider>(auth: A) -> Result<Self, error::Api>
{
Self::new_with_base("https://api.spond.com", auth).await
}
pub async fn new_with_base<A: auth::Provider>(base: &str, auth: A) -> Result<Self, error::Api> {
let base = reqwest::Url::parse(base)?;
let host = base.host_str().unwrap().to_owned();
let client = reqwest::Client::builder()
//.https_only(true)
.redirect(reqwest::redirect::Policy::limited(1))
.retry(reqwest::retry::for_host(host)
.max_retries_per_request(2)
.classify_fn(|req_rep| {
use reqwest::StatusCode;
match req_rep.status() {
Some(StatusCode::UNAUTHORIZED) => req_rep.retryable(),
_ => req_rep.success(),
}
}))
// TODO
//.cookie_store(true)
.user_agent("spond-selection-bot")
.read_timeout(std::time::Duration::from_secs(5))
.connect_timeout(std::time::Duration::from_secs(15))
.connection_verbose(true)
.build()?;
let api = Api(Internal::new(client.clone(), base.clone(), NoToken));
let provider = auth.authenticate(api).await?;
tracing::info!("{:?}", provider);
let api = Api(Internal::new(client, base, provider));
Ok(api)
}
pub async fn call<R: Request + Sized>(&self, request: &R) -> Result<R::Response, error::Api>
{
//async fn call<C: Call + ?Sized>(&self, api: &Api, call: &C, request: &C::Request) -> Result<C::Response, error::Call::<C>> {
// let mut token = self.token_provider.current();
// loop {
// match self.common.call(call, request, &token) {
// Err(error::Call::<C>::Http(reqwest::StatusCode::UNAUTHORIZED)) if token.is_some_and(|token|token.expired())
// => token = self.token_provider.refresh(api).map_err(error::Call::<C>::Token)?,
// result => return result,
// }
// }
//}
let client = self.0.client();
if R::Authorization::VALUE == false {
let url = self.0.url(request.endpoint())?;
call(client, url, request, None).await
} else {
let mut current = self.0.current_token();
loop {
use std::ops::Deref;
let token = if let Some(ref token) = current {
Some(token.deref())
} else {
None
};
let url = self.0.url(request.endpoint())?;
current = match call(client, url, request, token).await {
Err(error::Api::Http(reqwest::StatusCode::UNAUTHORIZED)) if current.is_some_and(|t|t.expired())
=> self.0.refresh_token(self.clone()).await?,
result => break result,
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn mock() {}
}