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

231
turnerbund/:w Normal file
View file

@ -0,0 +1,231 @@
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(;
}

25
turnerbund/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[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"

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() {}
}

185
turnerbund/src/api/auth.rs Normal file
View file

@ -0,0 +1,185 @@
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"}"#)
}
}
}

View file

@ -0,0 +1,24 @@
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;
}

View file

@ -0,0 +1,29 @@
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),
}

32
turnerbund/src/api/mod.rs Normal file
View file

@ -0,0 +1,32 @@
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)
}

View file

@ -0,0 +1,13 @@
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;
}

View file

@ -0,0 +1,38 @@
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>;
}

View file

@ -0,0 +1,10 @@
use reqwest_middleware::reqwest;
use reqwest_middleware::ClientWithMiddleware as Backend;
struct Client {
backend: Backend,
}
impl Client {
}

74
turnerbund/src/id.rs Normal file
View file

@ -0,0 +1,74 @@
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)
}
}

51
turnerbund/src/main.rs Normal file
View file

@ -0,0 +1,51 @@
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(())
}

166
turnerbund/src/select.rs Normal file
View file

@ -0,0 +1,166 @@
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(())
}
}

View file

@ -0,0 +1,48 @@
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> {}

View file

@ -0,0 +1,31 @@
// 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 {}

2
turnerbund/src/user.rs Normal file
View file

@ -0,0 +1,2 @@
struct Profile;
struct User;

19
turnerbund/src/utils.rs Normal file
View file

@ -0,0 +1,19 @@
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;
}