Compare commits
No commits in common. "main" and "old" have entirely different histories.
51 changed files with 2457 additions and 2339 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
2166
Cargo.lock
generated
Normal file
2166
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "spond"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = [ "derive" ] }
|
||||
reqwest = { version = "0.12", features = [ "json", "cookies" ] }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
serde = "1.0"
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
chrono = { version = "0.4", features = [ "serde" ] }
|
||||
serde_json = "1.0"
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
[package]
|
||||
name = "spond"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
reqwest = [ "dep:reqwest", "reqwest/json" ]
|
||||
log = [ "tracing/log" ]
|
||||
tracing = [ ]
|
||||
cookies = [ "reqwest?/cookies" ]
|
||||
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
bon = "3.9.0"
|
||||
bytes = "1.11.1"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
#macro = { path = "../macro" }
|
||||
reqwest = { version = "0.13.2", optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
#spond-auth-login = { version = "0.1.0", path = "../login" }
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.49.0" }
|
||||
tracing = { version = "0.1.44", features = [] }
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.49.0", features = ["macros"] }
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct Email {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Email {
|
||||
pub fn new(email: &str, password: &str) -> Self {
|
||||
Self {
|
||||
email: email.to_string(),
|
||||
password: password.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Credentials for Email {}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
mod email;
|
||||
mod phone;
|
||||
|
||||
pub use email::Email;
|
||||
pub use phone::Phone;
|
||||
|
||||
use crate::{
|
||||
Method,
|
||||
authentication::Tokens,
|
||||
traits::{authentication::login::Credentials, endpoint},
|
||||
};
|
||||
|
||||
pub struct Login;
|
||||
|
||||
impl<C: Credentials> endpoint::Public<C> for Login {
|
||||
const METHOD: Method = Method::POST;
|
||||
type Schema = Tokens;
|
||||
|
||||
fn path(&self, _: &C) -> &str {
|
||||
"core/v1/auth2/login"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct Phone {
|
||||
phone: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Phone {
|
||||
pub fn new(phone: &str, password: &str) -> Self {
|
||||
Self {
|
||||
phone: phone.to_string(),
|
||||
password: password.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Credentials for Phone {}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
pub mod login;
|
||||
pub mod token;
|
||||
|
||||
pub use login::Login;
|
||||
pub use token::Token;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tokens {
|
||||
pub access_token: token::Access,
|
||||
pub refresh_token: token::Refresh,
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use crate::traits::Request;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Access(Expire);
|
||||
|
||||
impl Deref for Access {
|
||||
type Target = Expire;
|
||||
|
||||
fn deref(&self) -> &Expire {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Refresh(Expire);
|
||||
|
||||
impl Deref for Refresh {
|
||||
type Target = Expire;
|
||||
|
||||
fn deref(&self) -> &Expire {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Token {
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl AsRef<str> for Token {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Expire {
|
||||
#[serde(flatten)]
|
||||
token: Token,
|
||||
#[serde(skip_serializing)]
|
||||
expiration: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Expire {
|
||||
pub fn expired(&self) -> bool {
|
||||
Utc::now() <= self.expiration
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Expire {
|
||||
type Target = Token;
|
||||
|
||||
fn deref(&self) -> &Token {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
trait Factory: Serialize {}
|
||||
impl Factory for Refresh {}
|
||||
impl Factory for Token {}
|
||||
|
||||
impl<R: Factory + Request> crate::traits::endpoint::Public<R> for R
|
||||
{
|
||||
const METHOD: crate::Method = crate::Method::POST;
|
||||
type Schema = super::Tokens;
|
||||
|
||||
fn path(&self, _: &R) -> &str {
|
||||
"auth2/refresh"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
pub struct True;
|
||||
pub struct False;
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
use thiserror::Error;
|
||||
|
||||
pub mod api {
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Refresh<Transfer, Decode> = Public<Transfer, Decode>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Private<Transfer: std::error::Error, Decode: std::error::Error> {
|
||||
#[error("endpoint url")]
|
||||
Url(url::ParseError),
|
||||
|
||||
#[error("json decode")]
|
||||
Decode(Decode),
|
||||
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("token refresh")] // TODO: serde_json::Error -> Schema::...?
|
||||
Refresh(Refresh<Transfer, serde_json::Error>),
|
||||
|
||||
#[error("send error")]
|
||||
Send(Transfer),
|
||||
|
||||
#[error("receive error")]
|
||||
Receive(Transfer),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Public<Transfer: std::error::Error, Decode: std::error::Error> {
|
||||
#[error("endpoint url")]
|
||||
Url(url::ParseError),
|
||||
|
||||
#[error("json decode")]
|
||||
Decode(Decode),
|
||||
|
||||
#[error("send error")]
|
||||
Send(Transfer),
|
||||
|
||||
#[error("receive error")]
|
||||
Receive(Transfer),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Id {
|
||||
#[error("invalid length")]
|
||||
Length(usize),
|
||||
|
||||
#[error("invalid symbol")]
|
||||
Parser(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Authorization {
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
//#[derive(Debug, Error)]
|
||||
//pub enum Authentication<A: std::error::Error, B: std::error::Error> {
|
||||
// #[error("unauthorized")]
|
||||
// Public(A),
|
||||
//
|
||||
// #[error("authorized")]
|
||||
// Private(B),
|
||||
//}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::{
|
||||
fmt,
|
||||
str::FromStr,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
utils::X128,
|
||||
traits::id::Marker,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Id<T: Marker> {
|
||||
#[serde(flatten)]
|
||||
id: X128,
|
||||
#[serde(skip)]
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Marker> Id<T> {
|
||||
pub fn new(value: impl Into<X128>) -> Self {
|
||||
Self {
|
||||
id: value.into(),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> Deref for Id<T> {
|
||||
type Target = X128;
|
||||
|
||||
fn deref(&self) -> &X128 {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> fmt::Display for Id<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> FromStr for Id<T> {
|
||||
type Err = <X128 as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
X128::from_str(s).map(Self::new)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
pub mod authentication;
|
||||
pub mod authorization;
|
||||
pub mod error;
|
||||
pub mod id;
|
||||
pub mod schema;
|
||||
pub mod traits;
|
||||
|
||||
mod utils;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Method {
|
||||
GET,
|
||||
POST,
|
||||
}
|
||||
|
||||
pub use id::Id;
|
||||
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
authentication,
|
||||
error,
|
||||
};
|
||||
use crate::traits::{
|
||||
client,
|
||||
endpoint,
|
||||
Request,
|
||||
Schema,
|
||||
};
|
||||
|
||||
pub struct Public {
|
||||
client: reqwest::Client,
|
||||
base: url::Url,
|
||||
}
|
||||
|
||||
pub struct Private {
|
||||
public: Public,
|
||||
tokens: tokio::sync::RwLock<authentication::Tokens>,
|
||||
}
|
||||
|
||||
impl Private {
|
||||
async fn current_token(&self) -> authentication::Token {
|
||||
let tokens = self.tokens.read().await;
|
||||
let token: &authentication::Token = (&*tokens).into();
|
||||
token.clone()
|
||||
}
|
||||
|
||||
async fn refresh_tokens(&self) -> Result<authentication::Token, error::api::Refresh<reqwest::Error, serde_json::Error>> {
|
||||
let mut tokens = self.tokens.write().await;
|
||||
*tokens = (*tokens).refresh(self).await?;
|
||||
let token: &authentication::Token = (&*tokens).into();
|
||||
Ok(token.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Client = Private;
|
||||
|
||||
|
||||
type AuthorizedError<S: Schema<R>, R: Request> = error::api::Private<reqwest::Error, S::Error>;
|
||||
type UnauthorizedError<S: Schema<R>, R: Request> = error::api::Public<reqwest::Error, S::Error>;
|
||||
|
||||
async fn authorized<R, S>(client: &reqwest::Client, method: reqwest::Method, base: &url::Url, path: &str, request: &R, token: &authentication::Token) -> Result<S, AuthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(AuthorizedError::<S, R>::Url)?;
|
||||
|
||||
let response = client.request(method, url)
|
||||
.json(request)
|
||||
.bearer_auth(token.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Send)?;
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response.text_with_charset("utf-8")
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
|
||||
S::deserialize(request, &body).map_err(AuthorizedError::<S, R>::Decode)
|
||||
}
|
||||
|
||||
async fn unauthorized<R, S>(client: &reqwest::Client, method: reqwest::Method, base: &url::Url, path: &str, request: &R) -> Result<S, UnauthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(UnauthorizedError::<S, R>::Url)?;
|
||||
|
||||
let response = client.request(method, url)
|
||||
.json(request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Send)?;
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response.text_with_charset("utf-8")
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
|
||||
S::deserialize(request, &body).map_err(UnauthorizedError::<S, R>::Decode)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Public {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(&self, endpoint: &E, request: &R) -> Result<E::Schema, error::api::Public<reqwest::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
unauthorized::<R, E::Schema>(&self.client, E::METHOD.into(), &self.base, endpoint.path(request), &request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(&self, endpoint: &E, request: &R) -> Result<E::Schema, error::api::Public<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
self.public.execute::<E, R>(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Private for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(&self, endpoint: &E, request: &R) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Private<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
let token = self.current_token().await;
|
||||
match authorized::<R, E::Schema>(&self.public.client, E::METHOD.into(), &self.public.base, endpoint.path(request), &request, &token).await {
|
||||
Err(AuthorizedError::<E::Schema, R>::Unauthorized) if token.expired() => {
|
||||
let token = self.refresh_tokens().await.map_err(AuthorizedError::<E::Schema, R>::Refresh)?;
|
||||
authorized::<R, E::Schema>(&self.public.client, E::METHOD.into(), &self.public.base, endpoint.path(request), &request, &token).await
|
||||
},
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
use crate::Method;
|
||||
pub use reqwest;
|
||||
|
||||
impl From<Method> for reqwest::Method {
|
||||
fn from(method: Method) -> Self {
|
||||
match method {
|
||||
Method::GET => reqwest::Method::GET,
|
||||
Method::POST => reqwest::Method::POST,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::traits::{Request, Schema, client, endpoint};
|
||||
use crate::{authentication, error};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Public {
|
||||
client: reqwest::Client,
|
||||
base: url::Url,
|
||||
}
|
||||
|
||||
impl Public {
|
||||
pub fn new() -> Result<Self, reqwest::Error> {
|
||||
let base = url::Url::parse("https://api.spond.com").unwrap();
|
||||
Self::new_with_base(base)
|
||||
}
|
||||
|
||||
pub fn new_with_base(base: url::Url) -> Result<Self, reqwest::Error> {
|
||||
let client = reqwest::Client::builder()
|
||||
.https_only(true)
|
||||
.redirect(reqwest::redirect::Policy::limited(1));
|
||||
#[cfg(feature = "cookies")]
|
||||
let client = client.cookie_store(true);
|
||||
#[cfg(feature = "tracing")]
|
||||
let client = client.connection_verbose(true);
|
||||
|
||||
let client = client
|
||||
//.user_agent("...")
|
||||
//.read_timeout(std::time::Duration)
|
||||
//.connect_timeout(std::time::Duration)
|
||||
.build()?;
|
||||
Ok(Self { client, base })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Authenticator for Public {
|
||||
type Private = Private;
|
||||
|
||||
fn with_tokens(&self, tokens: authentication::Tokens) -> Self::Private {
|
||||
Private {
|
||||
public: self.clone(),
|
||||
tokens: tokio::sync::RwLock::new(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Private {
|
||||
public: Public,
|
||||
tokens: tokio::sync::RwLock<authentication::Tokens>,
|
||||
}
|
||||
|
||||
impl Private {
|
||||
async fn current_token(&self) -> authentication::token::Access {
|
||||
let tokens = self.tokens.read().await;
|
||||
tokens.access_token.clone()
|
||||
}
|
||||
|
||||
async fn refresh_tokens(
|
||||
&self,
|
||||
) -> Result<authentication::token::Access, error::api::Refresh<reqwest::Error, serde_json::Error>> {
|
||||
use client::Public;
|
||||
let mut tokens = self.tokens.write().await;
|
||||
*tokens = self
|
||||
.public
|
||||
.execute(&tokens.refresh_token, &tokens.refresh_token)
|
||||
.await?;
|
||||
Ok((*tokens).access_token.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Client = Private;
|
||||
|
||||
type AuthorizedError<S, R> = error::api::Private<reqwest::Error, <S as Schema<R>>::Error>;
|
||||
type UnauthorizedError<S, R> = error::api::Public<reqwest::Error, <S as Schema<R>>::Error>;
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn authorized<R, S>(
|
||||
client: &reqwest::Client,
|
||||
method: reqwest::Method,
|
||||
base: &url::Url,
|
||||
path: &str,
|
||||
request: &R,
|
||||
token: &authentication::token::Access,
|
||||
) -> Result<S, AuthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(AuthorizedError::<S, R>::Url)?;
|
||||
|
||||
let builder = client
|
||||
.request(method, url)
|
||||
.json(request)
|
||||
.bearer_auth(token.as_ref());
|
||||
tracing::debug!("request: {builder:?}");
|
||||
|
||||
let response = builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Send)?;
|
||||
tracing::debug!("response: {response:?}");
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
tracing::debug!("body: {body:?}");
|
||||
|
||||
let result = S::deserialize(request, &body).map_err(AuthorizedError::<S, R>::Decode)?;
|
||||
tracing::debug!("result: {result:?}");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn unauthorized<R, S>(
|
||||
client: &reqwest::Client,
|
||||
method: reqwest::Method,
|
||||
base: &url::Url,
|
||||
path: &str,
|
||||
request: &R,
|
||||
) -> Result<S, UnauthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(UnauthorizedError::<S, R>::Url)?;
|
||||
|
||||
let builder = client
|
||||
.request(method, url)
|
||||
.json(request);
|
||||
tracing::debug!("request: {builder:?}");
|
||||
let response = builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Send)?;
|
||||
tracing::debug!("response: {response:?}");
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
tracing::debug!("body: {body:?}");
|
||||
|
||||
let result = S::deserialize(request, &body).map_err(UnauthorizedError::<S, R>::Decode)?;
|
||||
tracing::debug!("result: {result:?}");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Public {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Public<reqwest::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
unauthorized::<R, E::Schema>(
|
||||
&self.client,
|
||||
E::METHOD.into(),
|
||||
&self.base,
|
||||
endpoint.path(request),
|
||||
&request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Public<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
self.public.execute::<E, R>(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Private for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Private<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
let token = self.current_token().await;
|
||||
match authorized::<R, E::Schema>(
|
||||
&self.public.client,
|
||||
E::METHOD.into(),
|
||||
&self.public.base,
|
||||
endpoint.path(request),
|
||||
&request,
|
||||
&token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(AuthorizedError::<E::Schema, R>::Unauthorized) if token.expired() => {
|
||||
let token = self
|
||||
.refresh_tokens()
|
||||
.await
|
||||
.map_err(AuthorizedError::<E::Schema, R>::Refresh)?;
|
||||
authorized::<R, E::Schema>(
|
||||
&self.public.client,
|
||||
E::METHOD.into(),
|
||||
&self.public.base,
|
||||
endpoint.path(request),
|
||||
&request,
|
||||
&token,
|
||||
)
|
||||
.await
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
pub mod sponds;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// TODO: use crate::Id;
|
||||
use crate::traits;
|
||||
|
||||
// {
|
||||
// "heading" : "Kraftraum putzen",
|
||||
// "id" : "D678340FAA6341058E368AE2FB6082CA",
|
||||
// "series" : false,
|
||||
// "startTime" : "2026-11-07T08:00:00Z",
|
||||
// "unanswered" : true,
|
||||
// "updated" : 1770872490567
|
||||
// }
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Spond {
|
||||
pub id: crate::utils::X128,
|
||||
pub updated: crate::utils::Timestamp,
|
||||
pub start_time: chrono::DateTime<chrono::Utc>,
|
||||
pub heading: String,
|
||||
#[serde(default)]
|
||||
pub unanswered: bool,
|
||||
pub series: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Default)]
|
||||
pub struct Sponds(Vec<Spond>);
|
||||
|
||||
impl std::ops::Deref for Sponds {
|
||||
type Target = [Spond];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Upcoming;
|
||||
|
||||
impl traits::endpoint::Private<Upcoming> for Sponds {
|
||||
const METHOD: crate::Method = crate::Method::GET;
|
||||
type Schema = Self;
|
||||
|
||||
fn path(&self, _: &Upcoming) -> &str {
|
||||
"/core/v1/sponds/upcoming"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_upcoming_schema() {
|
||||
const DATA: &[u8] = include_bytes!("./sponds_upcoming.json");
|
||||
let bytes = bytes::Bytes::from_static(DATA);
|
||||
let v: Sponds = <Sponds as super::traits::Schema<Upcoming>>::deserialize(&Upcoming, &bytes).expect("VALID JSON");
|
||||
println!("{v:?}");
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
pub trait Credentials: super::super::Request {}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
pub mod login;
|
||||
pub use login::Credentials;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
pub trait Authorization: private::Seal {}
|
||||
|
||||
mod private {
|
||||
use crate::authorization::{False, True};
|
||||
pub trait Seal {}
|
||||
|
||||
impl Seal for True {}
|
||||
impl Seal for False {}
|
||||
}
|
||||
|
||||
impl<T: private::Seal> Authorization for T {}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
use super::{Authorization, Endpoint, Request, Schema, endpoint};
|
||||
use crate::{authorization, authentication, error};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub trait Client: Public + Private {}
|
||||
|
||||
impl<C: Public + Private> Client for C {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Handler<E: Endpoint<R, A>, R: Request, A: Authorization> {
|
||||
type Error: std::error::Error;
|
||||
|
||||
async fn execute(&self, endpoint: &E, request: &R) -> Result<E::Schema, Self::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Authenticator: Public {
|
||||
type Private: Private;
|
||||
|
||||
fn with_tokens(&self, tokens: authentication::Tokens) -> Self::Private;
|
||||
|
||||
async fn authenticate<E, R>(&self, endpoint: &E, request: &R) -> Result<Self::Private, error::api::Public<<Self as Public>::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R, Schema=authentication::Tokens> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
let tokens = self.execute(endpoint, request).await?;
|
||||
Ok(self.with_tokens(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Public {
|
||||
type Error: std::error::Error + Send;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Public<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P: Public + Sync, E: endpoint::Public<R> + Sync, R: Request + Sync>
|
||||
Handler<E, R, authorization::False> for P
|
||||
{
|
||||
type Error = error::api::Public<P::Error, <E::Schema as Schema<R>>::Error>;
|
||||
|
||||
async fn execute(&self, endpoint: &E, request: &R) -> Result<E::Schema, Self::Error> {
|
||||
self.execute(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Private {
|
||||
type Error: std::error::Error + Send;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Private<R> + Sync,
|
||||
R: Request + Sync;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P: Private + Sync, E: endpoint::Private<R> + Sync, R: Request + Sync>
|
||||
Handler<E, R, authorization::True> for P
|
||||
{
|
||||
type Error = error::api::Private<P::Error, <E::Schema as Schema<R>>::Error>;
|
||||
|
||||
async fn execute(&self, endpoint: &E, request: &R) -> Result<E::Schema, Self::Error> {
|
||||
self.execute(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
use super::{Authorization, Request, Schema};
|
||||
use crate::{Method, authorization};
|
||||
|
||||
pub trait Endpoint<R: Request, A: Authorization>: private::Seal<R, A> {
|
||||
const METHOD: Method;
|
||||
type Schema: Schema<R>;
|
||||
|
||||
fn path(&self, request: &R) -> &str;
|
||||
}
|
||||
|
||||
/// A Endpoint with required authorization
|
||||
pub trait Private<R: Request> {
|
||||
const METHOD: Method;
|
||||
type Schema: Schema<R>;
|
||||
|
||||
fn path(&self, request: &R) -> &str;
|
||||
}
|
||||
|
||||
impl<T: Private<R>, R: Request> Endpoint<R, authorization::True> for T {
|
||||
const METHOD: Method = T::METHOD;
|
||||
type Schema = T::Schema;
|
||||
|
||||
fn path(&self, request: &R) -> &str {
|
||||
self.path(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Endpoint without required authorization
|
||||
pub trait Public<R: Request> {
|
||||
const METHOD: Method;
|
||||
type Schema: Schema<R>;
|
||||
|
||||
fn path(&self, request: &R) -> &str;
|
||||
}
|
||||
|
||||
impl<T: Public<R>, R: Request> Endpoint<R, authorization::False> for T {
|
||||
const METHOD: Method = T::METHOD;
|
||||
type Schema = T::Schema;
|
||||
|
||||
fn path(&self, request: &R) -> &str {
|
||||
self.path(request)
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
/// Seal the Endpoint dependency
|
||||
pub trait Seal<R: Request, A: Authorization> {}
|
||||
|
||||
impl<R: Request, P: Private<R>> Seal<R, authorization::True> for P {}
|
||||
impl<R: Request, P: Public<R>> Seal<R, authorization::False> for P {}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
use super::{
|
||||
client::Handler,
|
||||
Schema,
|
||||
Endpoint,
|
||||
};
|
||||
use crate::{
|
||||
Id,
|
||||
authorization,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Marker: Schema<Id<Self>> + Endpoint<Id<Self>, authorization::True, Schema=Self> + std::fmt::Debug {
|
||||
async fn resolve<H: Handler<Self, Id<Self>, authorization::True> + Sync>(&self, id: &Id<Self>, handler: &H) -> Result<Self, H::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Schema<Id<T>> + Endpoint<Id<T>, authorization::True, Schema=T> + Sync + std::fmt::Debug> Marker for T {
|
||||
async fn resolve<H: Handler<Self, Id<Self>, authorization::True> + Sync>(&self, id: &Id<Self>, handler: &H) -> Result<Self, H::Error> {
|
||||
handler.execute(&self, id).await
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
pub mod authentication;
|
||||
pub mod authorization;
|
||||
pub mod client;
|
||||
pub mod endpoint;
|
||||
pub mod id;
|
||||
pub mod request;
|
||||
pub mod schema;
|
||||
|
||||
pub use authentication::Credentials;
|
||||
pub use authorization::Authorization;
|
||||
pub use client::Client;
|
||||
pub use endpoint::Endpoint;
|
||||
pub use request::Request;
|
||||
pub use schema::Schema;
|
||||
|
||||
//#[cfg(test)]
|
||||
//mod test;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
pub trait Request: Serialize + std::fmt::Debug {}
|
||||
|
||||
impl<T: Serialize + std::fmt::Debug> Request for T {}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use super::Request;
|
||||
use bytes::Bytes;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub trait Schema<R: Request>: std::fmt::Debug + Send + Sized {
|
||||
type Error: std::error::Error + Send;
|
||||
|
||||
fn deserialize(request: &R, response: &Bytes) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned + Send + std::fmt::Debug, R: Request> Schema<R> for T {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn deserialize(_: &R, response: &Bytes) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&response)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
struct Private;
|
||||
struct Endpoint;
|
||||
#[derive(serde::Serialize)]
|
||||
struct Request;
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Schema;
|
||||
|
||||
#[async_trait]
|
||||
impl super::client::Private for Private {
|
||||
type Error = error::Private;
|
||||
|
||||
async fn execute<E, R>(&self, _: &E, _: &R) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: super::endpoint::Private<R>,
|
||||
R: super::Request + Sync,
|
||||
{
|
||||
Err(error::Private::Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::endpoint::Private<Request> for Endpoint {
|
||||
const METHOD: reqwest::Method = reqwest::Method::GET;
|
||||
type Schema = Schema;
|
||||
|
||||
fn path(&self, _: &Request) -> &str {
|
||||
"/core/v1/endpoint/private"
|
||||
}
|
||||
}
|
||||
|
||||
mod error {
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Private {
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_endpoint() {
|
||||
use super::client::Private;
|
||||
let client = Private;
|
||||
let request = Request;
|
||||
let endpoint = Endpoint;
|
||||
let result = match client.execute(&endpoint, &request).await {
|
||||
Err(error::Private::Unauthorized) => false,
|
||||
_ => true,
|
||||
};
|
||||
assert_eq!(result, true)
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
mod x128;
|
||||
pub use x128::X128;
|
||||
|
||||
mod timestamp;
|
||||
pub use timestamp::Timestamp;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Timestamp(u64);
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
use serde::{Serialize, Deserialize, Serializer, Deserializer};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::fmt;
|
||||
|
||||
/// Wrapper for u128 that serializes/deserializes as 32-charachter hex
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct X128(u128);
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("invalid length: {0}")]
|
||||
Length(usize),
|
||||
|
||||
#[error("parser error: {0}")]
|
||||
Parser(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl X128 {
|
||||
/// construct a new value
|
||||
pub fn new(value: u128) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// access the inner value explicitely
|
||||
pub fn value(&self) -> u128 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for X128 {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
match s.len() {
|
||||
32 => u128::from_str_radix(s, 16)
|
||||
.map(Self::new)
|
||||
.map_err(Error::Parser),
|
||||
len => Err(Error::Length(len)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for X128 {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Error> {
|
||||
Self::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for X128 {
|
||||
type Target = u128;
|
||||
|
||||
fn deref(&self) -> &u128 {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for X128 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:032x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for X128 {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for X128 {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
Self::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
14
flake.lock
generated
14
flake.lock
generated
|
|
@ -2,16 +2,18 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770843696,
|
||||
"narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
|
||||
"owner": "NixOS",
|
||||
"lastModified": 1758427187,
|
||||
"narHash": "sha256-pHpxZ/IyCwoTQPtFIAG2QaxuSm8jWzrzBGjwQZIttJc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
|
||||
"rev": "554be6495561ff07b6c724047bdd7e0716aa7b46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
|
|
|
|||
66
flake.nix
66
flake.nix
|
|
@ -1,59 +1,17 @@
|
|||
{
|
||||
outputs = { nixpkgs, ... }: let
|
||||
spond = { rustPlatform, rustfmt, clippy, pkg-config, openssl, ... }: rustPlatform.buildRustPackage {
|
||||
pname = "spond";
|
||||
version = "0.0.0";
|
||||
description = "spond bot";
|
||||
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
];
|
||||
propagatedBuildInputs = [
|
||||
openssl.dev
|
||||
];
|
||||
};
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
||||
allpkgs = system: pkgs: pkgs.extend (_: _: nixpkgs.lib.attrsets.filterAttrs (name: _: name != "default") (packages system pkgs));
|
||||
|
||||
packages = system: pkgs': let
|
||||
pkgs = allpkgs system pkgs';
|
||||
in {
|
||||
default = pkgs.spond;
|
||||
spond = pkgs.callPackage spond {};
|
||||
};
|
||||
|
||||
devShells = system: pkgs': let
|
||||
pkgs = allpkgs system pkgs';
|
||||
in builtins.mapAttrs (devShell pkgs) (packages system pkgs');
|
||||
|
||||
devShell = pkgs: name: pkg: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
cargo-machete
|
||||
cargo-workspaces
|
||||
cargo-unused-features
|
||||
cargo-udeps
|
||||
cargo-audit
|
||||
cargo-diet
|
||||
cargo-duplicates
|
||||
cargo-flamegraph
|
||||
clippy
|
||||
|
||||
(python3.withPackages (py: [ py.pyyaml ]))
|
||||
|
||||
rustc
|
||||
rustfmt
|
||||
] ++ pkg.buildInputs;
|
||||
|
||||
nativeBuildInputs = pkg.nativeBuildInputs;
|
||||
|
||||
shellHook = ''
|
||||
printf 'Dev shell for %s ready!\n' '${pkg.name}'
|
||||
'';
|
||||
};
|
||||
in {
|
||||
packages = builtins.mapAttrs packages nixpkgs.legacyPackages;
|
||||
devShells = builtins.mapAttrs devShells nixpkgs.legacyPackages;
|
||||
outputs = { self, nixpkgs }: {
|
||||
devShells = builtins.mapAttrs (_: pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cargo
|
||||
openssl.dev
|
||||
pkg-config
|
||||
];
|
||||
};
|
||||
}) nixpkgs.legacyPackages;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
254
src/main.rs
Normal file
254
src/main.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
use reqwest::Client;
|
||||
use reqwest::ClientBuilder;
|
||||
use reqwest::RequestBuilder;
|
||||
use reqwest::Response;
|
||||
use reqwest::Method;
|
||||
use reqwest::Request;
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use log::*;
|
||||
|
||||
type Timestamp = chrono::DateTime<chrono::offset::Utc>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Login<'a> {
|
||||
email: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Token {
|
||||
token: String,
|
||||
expiration: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Tokens {
|
||||
access_token: Token,
|
||||
refresh_token: Token,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
struct Spond {
|
||||
client: Client,
|
||||
tokens: Tokens,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Order {
|
||||
Ascending,
|
||||
Descending,
|
||||
}
|
||||
|
||||
trait SpondRequest {
|
||||
type Response;
|
||||
|
||||
async fn request(&self, client: &Client) -> Result<RequestBuilder, Box<dyn std::error::Error>>;
|
||||
async fn parse(self, response: Response) -> Result<Self::Response, Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EventsRequest {
|
||||
query: std::vec::Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl SpondRequest for EventsRequest {
|
||||
type Response = std::vec::Vec<Event>;
|
||||
|
||||
async fn request(&self, client: &Client) -> Result<RequestBuilder, Box<dyn std::error::Error>> {
|
||||
let request = client.request(Method::GET, Spond::endpoint("sponds"))
|
||||
.query(&self.query);
|
||||
Ok(request)
|
||||
}
|
||||
async fn parse(self, response: Response) -> Result<Self::Response, Box<dyn std::error::Error>> {
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsRequest {
|
||||
pub fn new() -> Self {
|
||||
Self { query: std::vec::Vec::new() }
|
||||
}
|
||||
|
||||
fn add<K: std::string::ToString, V: std::string::ToString>(self, key: K, value: V) -> Self {
|
||||
let mut query = self.query;
|
||||
query.push((key.to_string(), value.to_string()));
|
||||
Self { query }
|
||||
}
|
||||
|
||||
pub fn comments(self, value: bool) -> Self {
|
||||
self.add("includeComments", value)
|
||||
}
|
||||
|
||||
pub fn hidden(self, value: bool) -> Self {
|
||||
self.add("includeHidden", value)
|
||||
}
|
||||
|
||||
pub fn profile_info(self, value: bool) -> Self {
|
||||
self.add("addProfileInfo", value)
|
||||
}
|
||||
|
||||
pub fn scheduled(self, value: bool) -> Self {
|
||||
self.add("scheduled", value)
|
||||
}
|
||||
|
||||
pub fn order(self, order: Order) -> Self {
|
||||
self.add("order", match order {
|
||||
Order::Ascending => "asc",
|
||||
Order::Descending => "dsc",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn max(self, count: u64) -> Self {
|
||||
self.add("max", count)
|
||||
}
|
||||
|
||||
pub fn min_end_timestamp(self, timestamp: Timestamp) -> Self {
|
||||
// TODO: make actual timestamp working
|
||||
self.add("minEndTimestamp", timestamp.date_naive())
|
||||
}
|
||||
}
|
||||
|
||||
impl Spond {
|
||||
fn endpoint(endpoint: &str) -> String {
|
||||
format!("https://api.spond.com/core/v1/{}", endpoint)
|
||||
}
|
||||
|
||||
pub async fn new(email: &str, password: &str) -> Result<Spond, Box<dyn std::error::Error>> {
|
||||
let client = ClientBuilder::new()
|
||||
.cookie_store(true)
|
||||
.https_only(true)
|
||||
.connection_verbose(false)
|
||||
.build()?;
|
||||
|
||||
// get the landing page for initial set of cookies
|
||||
let _ = client.get("https://spond.com/landing/login/").send().await?;
|
||||
|
||||
// try to log in
|
||||
let login = Login { email, password };
|
||||
let login = client.post(Spond::endpoint("auth2/login"))
|
||||
.json(&login)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let tokens: Tokens = login.json().await?;
|
||||
|
||||
let spond = Spond {
|
||||
client: client,
|
||||
tokens: tokens,
|
||||
};
|
||||
|
||||
Ok(spond)
|
||||
}
|
||||
|
||||
pub async fn send<U, T: SpondRequest::<Response=U>>(&self, info: T) -> Result<U, Box<dyn std::error::Error>> {
|
||||
let req = self.request(&info).await?;
|
||||
debug!("send: {req:#?}");
|
||||
let res = self.client.execute(req).await?;
|
||||
debug!("recv: {res:#?}");
|
||||
info.parse(res).await
|
||||
}
|
||||
|
||||
pub async fn request<T: SpondRequest>(&self, info: &T) -> Result<Request, Box<dyn std::error::Error>> {
|
||||
let request = info.request(&self.client)
|
||||
.await?
|
||||
.bearer_auth(&self.tokens.access_token.token)
|
||||
.build()?;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Event {
|
||||
id: String,
|
||||
heading: String,
|
||||
start_timestamp: Timestamp,
|
||||
end_timestamp: Timestamp,
|
||||
behalf_of_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn accept(self) -> EventReplyRequest {
|
||||
self.reply(true)
|
||||
}
|
||||
|
||||
pub fn decline(self) -> EventReplyRequest {
|
||||
self.reply(false)
|
||||
}
|
||||
|
||||
pub fn reply(self, accept: bool) -> EventReplyRequest {
|
||||
EventReplyRequest {
|
||||
event_id: self.id,
|
||||
user_id: self.behalf_of_ids[0].clone(),
|
||||
accept: accept,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EventReplyRequest {
|
||||
event_id: String,
|
||||
user_id: String,
|
||||
accept: bool,
|
||||
}
|
||||
|
||||
impl SpondRequest for EventReplyRequest {
|
||||
type Response = Response;
|
||||
|
||||
async fn request(&self, client: &Client) -> Result<RequestBuilder, Box<dyn std::error::Error>> {
|
||||
#[derive(Serialize)]
|
||||
struct Reply {
|
||||
accepted: bool,
|
||||
}
|
||||
let request = client.request(Method::PUT, Spond::endpoint(&format!("sponds/{0}/responses/{1}", &self.event_id, &self.user_id)))
|
||||
.json(&Reply { accepted: self.accept });
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
async fn parse(self, response: Response) -> Result<Self::Response, Box<dyn std::error::Error>> {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
trace!("trace");
|
||||
debug!("debug");
|
||||
info!("info");
|
||||
warn!("warn");
|
||||
error!("error");
|
||||
|
||||
let args = Cli::parse();
|
||||
|
||||
let spond = Spond::new(&args.email, &args.password).await?;
|
||||
|
||||
let events_request = EventsRequest::new()
|
||||
.comments(false)
|
||||
.hidden(false)
|
||||
.profile_info(false)
|
||||
.scheduled(true)
|
||||
.order(Order::Ascending)
|
||||
.max(20)
|
||||
.min_end_timestamp(chrono::Utc::now())
|
||||
;
|
||||
|
||||
let events = spond.send(events_request).await?;
|
||||
for event in events {
|
||||
if event.heading == "Krafttraining Donnerstag" && event.start_timestamp.date_naive() == chrono::Utc::now().date_naive() {
|
||||
let result = spond.send(event.accept()).await?;
|
||||
debug!("{result:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
231
turnerbund/:w
231
turnerbund/:w
|
|
@ -1,231 +0,0 @@
|
|||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
pub use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
#[cfg(feature="disabled")]
|
||||
mod disabled {
|
||||
|
||||
//mod select;
|
||||
//use select::WeightedSet;
|
||||
mod api;
|
||||
mod utils;
|
||||
mod id;
|
||||
mod user;
|
||||
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
pub use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
use rand::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NoError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
struct UserID(u128);
|
||||
|
||||
impl std::fmt::Display for UserID {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "UserID({})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Participants(Vec<(UserID, usize)>);
|
||||
|
||||
trait Event {
|
||||
fn registered(&self, uid: UserID) -> bool;
|
||||
fn participated(&self, uid: UserID) -> bool { self.registered(uid) }
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockEvent {
|
||||
id: usize,
|
||||
users: HashMap<UserID, bool>,
|
||||
}
|
||||
|
||||
impl MockEvent {
|
||||
fn new(id: usize, registrants: HashSet<UserID>, participants: HashSet<UserID>) -> Self {
|
||||
let users = registrants.into_iter()
|
||||
.map(|uid| (uid, participants.contains(&uid)))
|
||||
.collect();
|
||||
Self { id, users }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MockEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "MockEvent{}({:?})", self.id, self.users)
|
||||
}
|
||||
}
|
||||
|
||||
impl Event for &MockEvent {
|
||||
fn registered(&self, uid: UserID) -> bool {
|
||||
self.users.get(&uid).is_some()
|
||||
}
|
||||
|
||||
fn participated(&self, uid: UserID) -> bool {
|
||||
self.users.get(&uid).is_some_and(|participated|*participated)
|
||||
}
|
||||
}
|
||||
|
||||
impl Participants {
|
||||
pub fn add(mut self, uid: UserID) -> Self {
|
||||
self.0.push((uid, 1));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select<H>(mut self, history: H, rng: &mut impl Rng, count: usize)
|
||||
-> HashSet<UserID>
|
||||
where
|
||||
H: Iterator,
|
||||
<H as Iterator>::Item: Event,
|
||||
{
|
||||
for event in history {
|
||||
let mut modified = false;
|
||||
for item in self.0.iter_mut() {
|
||||
if event.registered(item.0) && !event.participated(item.0) {
|
||||
modified = true;
|
||||
item.1 += 1;
|
||||
}
|
||||
}
|
||||
if !modified {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
println!("{:?}", self.0);
|
||||
|
||||
self.0.sample_weighted(rng, count, |item: &'_ (_, usize)| item.1 as f64)
|
||||
.unwrap()
|
||||
.map(|item| item.0)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect<'a, I: spond::auth::Identifier + From<&'a str>>(id: &'a str) -> Result<spond::Api, spond::error::Api> {
|
||||
let pw = std::env::var("SPOND_PASSWORD").expect("a password is required in SPOND_PASSWORD");
|
||||
let id: I = id.into();
|
||||
let auth = spond::auth::Login::new(id, pw);
|
||||
let client = spond::Api::new(auth).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().pretty())
|
||||
.with(filter)
|
||||
.init();
|
||||
|
||||
error!("Error Message");
|
||||
warn!("Warn Message");
|
||||
info!("Info Message");
|
||||
debug!("Debug Message");
|
||||
trace!("Trace Message");
|
||||
|
||||
let client = if let Ok(email) = std::env::var("SPOND_EMAIL") {
|
||||
connect::<spond::auth::Email>(&email).await?
|
||||
} else if let Ok(phone) = std::env::var("SPOND_PHONE") {
|
||||
connect::<spond::auth::Phone>(&phone).await?
|
||||
} else {
|
||||
panic!("no credentials provided");
|
||||
};
|
||||
|
||||
let _ = client;
|
||||
|
||||
|
||||
let users = (0..25).map(UserID).collect::<Vec<UserID>>();
|
||||
let mut events = Vec::new();
|
||||
|
||||
for id in 0..5 {
|
||||
let mut rng = rand::rng();
|
||||
let want = users.iter().filter(|_| (&mut rng).random_bool(0.75)).map(|id|id.clone()).collect::<HashSet<UserID>>();
|
||||
|
||||
let mut participants = Participants::default();
|
||||
for uid in want.iter() {
|
||||
participants = participants.add(*uid);
|
||||
}
|
||||
|
||||
let participants = participants.select(events.iter().rev(), &mut rng, 12);
|
||||
let event = MockEvent::new(id, want, participants);
|
||||
println!("{event}");
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
for uid in users.into_iter() {
|
||||
let (registered, participated) = events.iter()
|
||||
.fold((0, 0), |(registered, participated), event| {
|
||||
let registered = registered + if event.registered(uid) { 1 } else { 0 };
|
||||
let participated = participated + if event.participated(uid) { 1 } else { 0 };
|
||||
(registered, participated)
|
||||
});
|
||||
|
||||
println!("{uid}: ({participated}/{registered}) {:.2}%",
|
||||
(participated as f64) / (registered as f64) * 100f64);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn tracing_setup(filter: &str) {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new(filter));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().pretty())
|
||||
.with(filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
async fn connect<A: spond::traits::client::Authenticator + Sync, C: spond::traits::authentication::login::Credentials + Sync>(authenticator: A, credentials: C) -> Result<A::Private, spond::error::api::Public<<A as spond::traits::client::Public>::Error, <spond::authentication::Tokens as spond::traits::Schema<C>>::Error>> {
|
||||
let auth = spond::authentication::Login;
|
||||
|
||||
authenticator.authenticate(&auth, &credentials).await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Spond()
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Sponds(Vec<Spond>);
|
||||
|
||||
impl spond::traits::endpoint::Private<SpondsQuery> for Sponds {
|
||||
const METHOD: spond::Method = spond::Method::GET;
|
||||
type Schema = Self;
|
||||
|
||||
fn path(&self, _: &SpodsW
|
||||
}
|
||||
|
||||
#[derive(serde::Seralize)]
|
||||
struct SpondsQuery;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_setup("info");
|
||||
|
||||
#[cfg(feature="disabled")]
|
||||
disabled::main().await;
|
||||
|
||||
error!("Error Message");
|
||||
warn!("Warn Message");
|
||||
info!("Info Message");
|
||||
debug!("Debug Message");
|
||||
trace!("Trace Message");
|
||||
|
||||
let pw = std::env::var("SPOND_PASSWORD").expect("a password is required in SPOND_PASSWORD");
|
||||
let client = spond::reqwest::Public::new().expect("public client");
|
||||
let client = if let Ok(email) = std::env::var("SPOND_EMAIL") {
|
||||
connect(client, spond::authentication::login::Email::new(&email, &pw)).await
|
||||
} else if let Ok(phone) = std::env::var("SPOND_PHONE") {
|
||||
connect(client, spond::authentication::login::Phone::new(&phone, &pw)).await
|
||||
} else {
|
||||
panic!("no credentials provided");
|
||||
};
|
||||
|
||||
let _ = client.execute(;
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
[package]
|
||||
name = "turnerbund"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
disabled=[]
|
||||
|
||||
[dependencies]
|
||||
spond = { path = "../api", features = [ "reqwest" ] }
|
||||
anyhow = "1.0.101"
|
||||
async-trait = "0.1.89"
|
||||
bon = "3.9.0"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
http = "1.4.0"
|
||||
rand = "0.10.0"
|
||||
#reqwest = { version = "0.13.2", features = ["json", "zstd", "brotli", "gzip", "deflate"] }
|
||||
reqwest-middleware = { version = "0.5.1", features = ["json", "http2"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "tracing"] }
|
||||
tracing = { version = "0.1.44", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3.22", features = ["serde", "json", "tracing", "env-filter", "chrono"] }
|
||||
url = "2.5.8"
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
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() {}
|
||||
}
|
||||
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
use crate::utils::False;
|
||||
|
||||
use super::{
|
||||
Request,
|
||||
error,
|
||||
};
|
||||
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
Serializer,
|
||||
ser::SerializeStruct,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
//#[error(transparent)]
|
||||
//Provider(P)
|
||||
#[error("invalid credentials")]
|
||||
Credentials,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Provider {
|
||||
type TokenProvider: super::token::Provider + 'static;
|
||||
|
||||
async fn authenticate(&self, api: super::Api) -> Result<Self::TokenProvider, error::Api>;
|
||||
}
|
||||
|
||||
pub trait Identifier: Sync + Send + Serialize + std::fmt::Debug {
|
||||
const KEY: &'static str;
|
||||
}
|
||||
|
||||
macro_rules! transparent_identifier {
|
||||
($name:ident, $key:expr) => {
|
||||
transparent_string!($name);
|
||||
|
||||
impl Identifier for $name {
|
||||
const KEY: &'static str = $key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! transparent_string {
|
||||
($name:ident) => {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl From<&str> for $name {
|
||||
fn from(s: &str) -> Self {
|
||||
s.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for $name {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a $name> for &'a str {
|
||||
fn from(id: &'a $name) -> Self {
|
||||
&id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for String {
|
||||
fn from(id: $name) -> Self {
|
||||
id.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
transparent_string!(Password);
|
||||
transparent_identifier!(Email, "email");
|
||||
transparent_identifier!(Phone, "phone");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Login<I: Identifier> {
|
||||
identifier: I,
|
||||
password: Password,
|
||||
}
|
||||
|
||||
impl<I: Identifier> Login<I> {
|
||||
pub fn new<P: Into<Password>>(identifier: I, password: P) -> Self {
|
||||
Self {
|
||||
identifier: identifier,
|
||||
password: password.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Identifier> Serialize for Login<I> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let mut state = serializer.serialize_struct("Login", 2)?;
|
||||
state.serialize_field(I::KEY, &self.identifier)?;
|
||||
state.serialize_field("password", &self.password)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<I: Identifier> Provider for Login<I> {
|
||||
type TokenProvider = TokenProvider;
|
||||
|
||||
async fn authenticate(&self, api: super::Api) -> Result<Self::TokenProvider, error::Api> {
|
||||
Ok(api.call(self).await?.into())
|
||||
//todo!("implement me")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tokens {
|
||||
access_token: super::token::Token,
|
||||
refresh_token: super::token::Token,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TokenProvider {
|
||||
access: std::sync::RwLock<std::sync::Arc<super::token::Token>>,
|
||||
refresh: std::sync::Mutex<super::token::Token>,
|
||||
}
|
||||
|
||||
impl From<Tokens> for TokenProvider {
|
||||
fn from(tokens: Tokens) -> Self {
|
||||
use std::sync::{Mutex,RwLock, Arc};
|
||||
Self {
|
||||
access: RwLock::new(Arc::new(tokens.access_token)),
|
||||
refresh: Mutex::new(tokens.refresh_token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::token::Provider for TokenProvider {
|
||||
fn current(&self) -> Option<std::sync::Arc<super::token::Token>> {
|
||||
Some(self.access.read().ok()?.clone())
|
||||
}
|
||||
|
||||
async fn refresh(&self, api: super::Api) -> Result<Option<std::sync::Arc<super::token::Token>>, super::error::Token> {
|
||||
// TODO
|
||||
let _ = api;
|
||||
Err(super::error::Token::Expired(chrono::TimeDelta::seconds(-5)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Identifier> Request for Login<I> {
|
||||
const METHOD: super::reqwest::Method = super::reqwest::Method::POST;
|
||||
type Response = Tokens;
|
||||
type Authorization = False;
|
||||
|
||||
fn endpoint(&self) -> &str {
|
||||
"core/v1/auth2/login"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod serialize {
|
||||
use super::super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn email_login() {
|
||||
let login = Login::<Email>::new("user@spond.com", "secret");
|
||||
|
||||
let json = serde_json::to_string(&login).unwrap();
|
||||
assert_eq!(json, r#"{"email":"user@spond.com","password":"secret"}"#)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phone_login() {
|
||||
let login = Login::<Phone>::new("+1234567890", "secret");
|
||||
|
||||
let json = serde_json::to_string(&login).unwrap();
|
||||
assert_eq!(json, r#"{"phone":"+1234567890","password":"secret"}"#)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub trait Method {
|
||||
const HTTP: http::Method;
|
||||
}
|
||||
|
||||
macro_rules! method {
|
||||
($name:ident) => {
|
||||
struct $name;
|
||||
|
||||
impl Method for $name {
|
||||
const HTTP = http::Method::$name;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
method!(GET);
|
||||
method!(POST);
|
||||
|
||||
pub trait Endpoint<M: Method>: Serialize {
|
||||
type Result: Deserialize;
|
||||
|
||||
fn path(&self) -> &str;
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
pub trait Error: std::error::Error + std::fmt::Debug + Send + Sync + 'static {}
|
||||
impl <E: std::error::Error + std::fmt::Debug + Send + Sync + 'static> Error for E {}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Api {
|
||||
#[error("reqwest")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
#[error("encode")]
|
||||
Encode(serde_json::Error),
|
||||
|
||||
#[error("decode")]
|
||||
Decode(serde_json::Error),
|
||||
|
||||
#[error("url")]
|
||||
Url(#[from] url::ParseError),
|
||||
|
||||
#[error("token")]
|
||||
Token(#[from] Token),
|
||||
|
||||
#[error("http")]
|
||||
Http(reqwest::StatusCode),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Token {
|
||||
#[error("expired token")]
|
||||
Expired(chrono::TimeDelta),
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
pub mod auth;
|
||||
pub mod token;
|
||||
pub mod error;
|
||||
pub mod request;
|
||||
mod api;
|
||||
pub use api::Api;
|
||||
pub use request::{
|
||||
Request,
|
||||
};
|
||||
|
||||
use reqwest_middleware::reqwest;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error<Auth: std::error::Error + Send + Sync + 'static> {
|
||||
#[error("unspecified")]
|
||||
Unspecified,
|
||||
|
||||
#[error("reqwest error")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
#[error("middleware error")]
|
||||
Middleware(anyhow::Error),
|
||||
|
||||
#[error("url parser error")]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
|
||||
#[error("http status code")]
|
||||
Http(reqwest::StatusCode),
|
||||
|
||||
#[error(transparent)]
|
||||
Custom(Auth)
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
use crate::utils::Bool;
|
||||
|
||||
trait Authorization: Bool {}
|
||||
|
||||
impl<T: Bool> Authorization for T {}
|
||||
|
||||
pub trait Request: Sized + std::fmt::Debug + serde::Serialize {
|
||||
const METHOD: reqwest::Method = reqwest::Method::GET;
|
||||
type Authorization: Authorization;
|
||||
type Response: for<'a> serde::Deserialize<'a> + Sized;
|
||||
|
||||
fn endpoint(&self) -> &str;
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct Token {
|
||||
token: String,
|
||||
expiration: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl AsRef<str> for Token {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn expired(&self) -> bool {
|
||||
self.expiration < chrono::Utc::now()
|
||||
}
|
||||
|
||||
pub fn expired_in(&self, offset: chrono::TimeDelta) -> bool {
|
||||
self.time_delta() < offset
|
||||
}
|
||||
|
||||
pub fn time_delta(&self) -> chrono::TimeDelta {
|
||||
self.expiration - chrono::Utc::now()
|
||||
}
|
||||
|
||||
pub fn r#use(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
builder.bearer_auth(&self.token)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Provider: std::fmt::Debug + Send + Sync {
|
||||
fn current(&self) -> Option<std::sync::Arc<Token>>;
|
||||
|
||||
async fn refresh(&self, api: super::Api) -> Result<Option<std::sync::Arc<Token>>, super::error::Token>;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
use reqwest_middleware::reqwest;
|
||||
use reqwest_middleware::ClientWithMiddleware as Backend;
|
||||
|
||||
struct Client {
|
||||
backend: Backend,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use serde::{Serialize, Deserialize, Serializer, Deserializer};
|
||||
use serde::de;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use super::api::Api;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
Length(usize),
|
||||
IntError(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for ParseError {
|
||||
fn from(e: std::num::ParseIntError) -> Self {
|
||||
Self::IntError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Length(len) => write!(f, "Id strig must be 32 characters, got {}", len),
|
||||
Self::IntError(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Marker {
|
||||
async fn resolve(id: Id<Self>, api: Api) -> Result<Self, super::api::error::Api>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Id<T: Marker + ?Sized>(u128, std::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Marker> Id<T> {
|
||||
pub fn new(value: u128) -> Self {
|
||||
Self(value, std::marker::PhantomData)
|
||||
}
|
||||
|
||||
pub fn value(&self) -> u128 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> fmt::Display for Id<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:032X}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> FromStr for Id<T> {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.len() {
|
||||
32 => Ok(Self::new(u128::from_str_radix(s, 16)?)),
|
||||
len => Err(ParseError::Length(len)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> Serialize for Id<T> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Marker> Deserialize<'de> for Id<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
String::deserialize(deserializer)?
|
||||
.parse()
|
||||
.map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
pub use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
fn tracing_setup(filter: &str) {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new(filter));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().pretty())
|
||||
.with(filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
async fn connect<A: spond::traits::client::Authenticator + Sync, C: spond::traits::authentication::login::Credentials + Sync>(authenticator: A, credentials: C) -> Result<A::Private, spond::error::api::Public<<A as spond::traits::client::Public>::Error, <spond::authentication::Tokens as spond::traits::Schema<C>>::Error>> {
|
||||
let auth = spond::authentication::Login;
|
||||
|
||||
authenticator.authenticate(&auth, &credentials).await
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_setup("info");
|
||||
|
||||
#[cfg(feature="disabled")]
|
||||
disabled::main().await;
|
||||
|
||||
error!("Error Message");
|
||||
warn!("Warn Message");
|
||||
info!("Info Message");
|
||||
debug!("Debug Message");
|
||||
trace!("Trace Message");
|
||||
|
||||
let pw = std::env::var("SPOND_PASSWORD").expect("a password is required in SPOND_PASSWORD");
|
||||
let client = spond::reqwest::Public::new().expect("public client");
|
||||
let client = if let Ok(email) = std::env::var("SPOND_EMAIL") {
|
||||
connect(client, spond::authentication::login::Email::new(&email, &pw)).await
|
||||
} else if let Ok(phone) = std::env::var("SPOND_PHONE") {
|
||||
connect(client, spond::authentication::login::Phone::new(&phone, &pw)).await
|
||||
} else {
|
||||
panic!("no credentials provided");
|
||||
}?;
|
||||
|
||||
|
||||
use spond::traits::client::Private;
|
||||
|
||||
let _ = client.execute(&spond::schema::sponds::Sponds::default(), &spond::schema::sponds::Upcoming).await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
use rand::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
struct UserID(u128);
|
||||
|
||||
impl std::fmt::Display for UserID {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "UserID({})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Participants(Vec<(UserID, usize)>);
|
||||
|
||||
trait Event {
|
||||
fn registered(&self, uid: UserID) -> bool;
|
||||
fn participated(&self, uid: UserID) -> bool { self.registered(uid) }
|
||||
}
|
||||
|
||||
impl Participants {
|
||||
pub fn add(mut self, uid: UserID) -> Self {
|
||||
self.0.push((uid, 1));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select<I: Event>(mut self, history: IntoIterator<Item=I>, rng: &mut impl Rng, count: usize)
|
||||
-> HashSet<UserID>
|
||||
{
|
||||
for event in history {
|
||||
let mut modified = false;
|
||||
for item in self.0.iter_mut() {
|
||||
if event.registered(item.0) && !event.participated(item.0) {
|
||||
modified = true;
|
||||
item.1 += 1;
|
||||
}
|
||||
}
|
||||
if !modified {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
println!("{:?}", self.0);
|
||||
|
||||
self.0.sample_weighted(rng, count, |item: &'_ (_, usize)| item.1 as f64)
|
||||
.unwrap()
|
||||
.map(|item| item.0)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature="disabled")]
|
||||
mod disabled {
|
||||
|
||||
//mod select;
|
||||
//use select::WeightedSet;
|
||||
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
pub use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NoError {}
|
||||
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockEvent {
|
||||
id: usize,
|
||||
users: HashMap<UserID, bool>,
|
||||
}
|
||||
|
||||
impl MockEvent {
|
||||
fn new(id: usize, registrants: HashSet<UserID>, participants: HashSet<UserID>) -> Self {
|
||||
let users = registrants.into_iter()
|
||||
.map(|uid| (uid, participants.contains(&uid)))
|
||||
.collect();
|
||||
Self { id, users }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MockEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "MockEvent{}({:?})", self.id, self.users)
|
||||
}
|
||||
}
|
||||
|
||||
impl Event for &MockEvent {
|
||||
fn registered(&self, uid: UserID) -> bool {
|
||||
self.users.get(&uid).is_some()
|
||||
}
|
||||
|
||||
fn participated(&self, uid: UserID) -> bool {
|
||||
self.users.get(&uid).is_some_and(|participated|*participated)
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect<'a, I: spond::auth::Identifier + From<&'a str>>(id: &'a str) -> Result<spond::Api, spond::error::Api> {
|
||||
let pw = std::env::var("SPOND_PASSWORD").expect("a password is required in SPOND_PASSWORD");
|
||||
let id: I = id.into();
|
||||
let auth = spond::auth::Login::new(id, pw);
|
||||
let client = spond::Api::new(auth).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().pretty())
|
||||
.with(filter)
|
||||
.init();
|
||||
|
||||
error!("Error Message");
|
||||
warn!("Warn Message");
|
||||
info!("Info Message");
|
||||
debug!("Debug Message");
|
||||
trace!("Trace Message");
|
||||
|
||||
let client = if let Ok(email) = std::env::var("SPOND_EMAIL") {
|
||||
connect::<spond::auth::Email>(&email).await?
|
||||
} else if let Ok(phone) = std::env::var("SPOND_PHONE") {
|
||||
connect::<spond::auth::Phone>(&phone).await?
|
||||
} else {
|
||||
panic!("no credentials provided");
|
||||
};
|
||||
|
||||
let _ = client;
|
||||
|
||||
|
||||
let users = (0..25).map(UserID).collect::<Vec<UserID>>();
|
||||
let mut events = Vec::new();
|
||||
|
||||
for id in 0..5 {
|
||||
let mut rng = rand::rng();
|
||||
let want = users.iter().filter(|_| (&mut rng).random_bool(0.75)).map(|id|id.clone()).collect::<HashSet<UserID>>();
|
||||
|
||||
let mut participants = Participants::default();
|
||||
for uid in want.iter() {
|
||||
participants = participants.add(*uid);
|
||||
}
|
||||
|
||||
let participants = participants.select(events.iter().rev(), &mut rng, 12);
|
||||
let event = MockEvent::new(id, want, participants);
|
||||
println!("{event}");
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
for uid in users.into_iter() {
|
||||
let (registered, participated) = events.iter()
|
||||
.fold((0, 0), |(registered, participated), event| {
|
||||
let registered = registered + if event.registered(uid) { 1 } else { 0 };
|
||||
let participated = participated + if event.participated(uid) { 1 } else { 0 };
|
||||
(registered, participated)
|
||||
});
|
||||
|
||||
println!("{uid}: ({participated}/{registered}) {:.2}%",
|
||||
(participated as f64) / (registered as f64) * 100f64);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use super::Key;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Candidate<T> {
|
||||
item: T,
|
||||
key: Key,
|
||||
}
|
||||
|
||||
impl<T> Candidate<T> {
|
||||
pub fn new_with_key(item: T, key: Key) -> Self {
|
||||
Self { item, key }
|
||||
}
|
||||
|
||||
pub fn new<R, F, W>(item: T, rng: R, weight: W) -> Option<Self>
|
||||
where
|
||||
R: FnOnce() -> F,
|
||||
F: Into<f64>,
|
||||
W: Into<f64>,
|
||||
{
|
||||
Key::new(rng, weight).map(|key|Self::new_with_key(item, key))
|
||||
}
|
||||
|
||||
pub fn item(self) -> T {
|
||||
self.item
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Ord for Candidate<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// Reverse for max-heap: largest key is "greatest"
|
||||
other.key.partial_cmp(&self.key).unwrap().reverse()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialOrd for Candidate<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for Candidate<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.key == other.key
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for Candidate<T> {}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Key newtype wrapping a f64 in the range [0.0,1.0]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub struct Key(f64);
|
||||
|
||||
impl Key {
|
||||
pub fn new<R, F, W>(rng: R, weight: W) -> Option<Self>
|
||||
where
|
||||
R: FnOnce() -> F,
|
||||
F: Into<f64>,
|
||||
W: Into<f64>,
|
||||
{
|
||||
let weight = weight.into();
|
||||
|
||||
if weight.is_finite() && weight > 0.0 {
|
||||
let u = -rng().into().ln();
|
||||
Some(Key((u / weight).recip()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Key {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// we are always in range [0.0,1.0]
|
||||
self.0.partial_cmp(&other.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Key {}
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
struct Profile;
|
||||
struct User;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
trait BoolValue {}
|
||||
|
||||
pub trait Bool: BoolValue {
|
||||
const VALUE: bool;
|
||||
}
|
||||
|
||||
pub struct True;
|
||||
pub struct False;
|
||||
|
||||
impl BoolValue for True {}
|
||||
impl BoolValue for False {}
|
||||
|
||||
impl Bool for True {
|
||||
const VALUE: bool = true;
|
||||
}
|
||||
|
||||
impl Bool for False {
|
||||
const VALUE: bool = false;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue