This commit is contained in:
Jonas Rabenstein 2026-03-05 02:05:32 +01:00
commit b09e8eafbb
16 changed files with 2531 additions and 258 deletions

View file

@ -1,5 +1,5 @@
[package]
name = "tb-rs"
name = "tb-spond-rs"
version = "0.1.0"
edition = "2024"
@ -20,7 +20,6 @@ serde_json = "1.0.149"
serde_qs = "1.0.0"
spond-api = { version = "0.1.0", path = "../api" }
thiserror = "2.0.18"
#spond-macros = { version = "0.1.0", path = "../macros" }
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
url = "2.5.8"
xdg = "3.0.0"

View file

@ -1,7 +1,7 @@
use clap::{Args, ArgGroup};
use anyhow::{Error, Result};
use clap::{ArgGroup, Args};
use restson::RestClient;
use anyhow::{Result, Error};
use std::str::{FromStr};
use std::str::FromStr;
#[derive(Args, Debug)]
#[command(group(
@ -29,13 +29,24 @@ fn bearer(mut client: RestClient, token: &str) -> Result<RestClient> {
impl Authentication {
pub async fn apply(&self, client: RestClient) -> Result<RestClient> {
let client = match (self.access.as_ref(), self.refresh.as_ref(), self.email.as_ref(), self.phone.as_ref()) {
(Some(ref v), None, None, None) => v.apply(client)?,
(None, Some(ref v), None, None) => v.apply(client).await?,
(None, None, Some(ref v), None) => v.apply(client).await?,
(None, None, None, Some(ref v)) => v.apply(client).await?,
let client = match (
self.access.as_ref(),
self.refresh.as_ref(),
self.email.as_ref(),
self.phone.as_ref(),
) {
(Some(v), None, None, None) => v.apply(client)?,
(None, Some(v), None, None) => v.apply(client).await?,
(None, None, Some(v), None) => v.apply(client).await?,
(None, None, None, Some(v)) => v.apply(client).await?,
(None, None, None, None) => client,
(a, b, c, d) => anyhow::bail!("invalid authentication: {} + {} + {} + {}", a.is_some(), b.is_some(), c.is_some(), d.is_some()),
(a, b, c, d) => anyhow::bail!(
"invalid authentication: {} + {} + {} + {}",
a.is_some(),
b.is_some(),
c.is_some(),
d.is_some()
),
};
Ok(client)
}
@ -63,12 +74,13 @@ impl FromStr for WithPassword {
struct Email(WithPassword);
impl Email {
async fn apply(&self, client: RestClient) -> Result<RestClient> {
let tokens = spond_api::authentication::email(&client, &self.0.value, &self.0.password).await?;
let tokens =
spond_api::authentication::email(&client, &self.0.value, &self.0.password).await?;
bearer(client, tokens.access.token.as_ref())
}
}
impl FromStr for Email {
type Err= <WithPassword as FromStr>::Err;
type Err = <WithPassword as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(WithPassword::from_str(s)?))
@ -79,12 +91,13 @@ impl FromStr for Email {
struct Phone(WithPassword);
impl Phone {
async fn apply(&self, client: RestClient) -> Result<RestClient> {
let tokens = spond_api::authentication::phone(&client, &self.0.value, &self.0.password).await?;
let tokens =
spond_api::authentication::phone(&client, &self.0.value, &self.0.password).await?;
bearer(client, tokens.access.token.as_ref())
}
}
impl FromStr for Phone {
type Err= <WithPassword as FromStr>::Err;
type Err = <WithPassword as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(WithPassword::from_str(s)?))

View file

@ -1,8 +1,8 @@
use anyhow::Result;
use clap::Parser;
use restson::RestClient;
use anyhow::Result;
use url::Url;
use spond_api as api;
use url::Url;
use xdg::BaseDirectories as xdg;
mod authentication;
@ -33,7 +33,7 @@ impl Cli {
pub async fn client(&self) -> Result<RestClient> {
let base = self.base.join("/core/v1/")?;
let client = RestClient::new(base.as_str())?;
Ok(self.authentication.apply(client).await?)
self.authentication.apply(client).await
}
}
@ -42,7 +42,7 @@ struct Seed(u64);
impl Default for Seed {
fn default() -> Self {
use rand::{rng, RngExt};
use rand::{RngExt, rng};
Self(rng().random())
}
}
@ -68,28 +68,27 @@ impl std::fmt::Display for Seed {
}
impl Seed {
pub fn shuffle<'a, T, F, W>(&self, input: &'a [T], weight: F) -> Result<Vec<T>>
pub fn shuffle<T, F, W>(&self, input: &[T], weight: F) -> Result<Vec<T>>
where
F: Fn(T) -> W,
W: Into<f64>,
T: Copy
T: Copy,
{
use rand::{SeedableRng, rngs::StdRng};
let len = input.len();
let sample = rand::seq::index::sample_weighted(
&mut StdRng::seed_from_u64(self.0),
len,
|idx| weight(input[idx]),
len,
)?;
log::debug!("sample: {:?}", sample);
Ok(sample.into_iter().map(move |idx| input[idx]).collect())
}
}
#[derive(Debug)]
struct Weights<Id>(std::collections::HashMap<Id, usize>);
@ -135,33 +134,30 @@ where
impl<Id> Weights<Id>
where
Id: Eq + Hash + Copy + serde::de::DeserializeOwned
Id: Eq + Hash + Copy + serde::de::DeserializeOwned,
{
pub fn load(serie: api::SeriesId) -> Result<Self> {
let path = Self::path(serie)?;
log::debug!("load {path:?}");
let file = std::fs::OpenOptions::new()
.read(true)
.open(path)?;
let file = std::fs::OpenOptions::new().read(true).open(path)?;
let data: std::collections::HashMap<Id, usize> = serde_json::from_reader(file)?;
Ok(Self(data))
}
}
impl<Id> Weights<Id>
where
Id: Eq + Hash + Copy + serde::Serialize
Id: Eq + Hash + Copy + serde::Serialize,
{
pub fn store(&self, serie: api::SeriesId) -> Result<()> {
use std::fs::{File, rename};
use std::io::{BufWriter, Write};
let path = Self::path(serie)?;
log::debug!("store {path:?}");
let tmp = path.with_extension("json.tmp");
log::trace!("temporary: {tmp:?}");
// create temporary file
let file = File::create(&tmp)?;
@ -180,6 +176,7 @@ where
drop(writer);
// atomic replace old file
log::trace!("rename {tmp:?} -> {path:?}");
rename(&tmp, &path)?;
Ok(())
@ -195,12 +192,14 @@ async fn main() -> Result<()> {
let series = cli.series.map(api::SeriesId::new);
let heading = cli.heading.as_ref();
let vip: Vec<api::MemberId> = if let Some(ref vip) = cli.vip {
vip.into_iter().map(|id| api::MemberId::new(*id)).collect()
vip.iter().map(|id| api::MemberId::new(*id)).collect()
} else {
[
0xEB07B45E45E6449386E70A7411816B6Fu128,
0xD05F8574AC544C8DB1A7DC5B6347AA49u128,
].map(|x| api::MemberId::new(x.into())).into()
]
.map(|x| api::MemberId::new(x.into()))
.into()
};
let client = cli.client().await?;
let client = &client;
@ -213,189 +212,123 @@ async fn main() -> Result<()> {
log::info!("heading: {heading}");
}
if true {
let now = chrono::Utc::now();
let sponds = api::spond::search()
.include_comments(true)
.order(api::Order::Ascending)
.max(1000)
.min_start_timestamp(now)
.max_end_timestamp(now + chrono::Duration::weeks(1))
.call(client).await?;
let now = chrono::Utc::now();
let sponds = api::spond::search()
.include_comments(true)
.order(api::Order::Ascending)
.max(1000)
.min_start_timestamp(now)
.max_end_timestamp(now + chrono::Duration::weeks(1))
.call(client)
.await?;
for spond in sponds.iter()
.filter(|spond| {
let result = series.is_none_or(|series| spond.series_id.is_some_and(|remote| remote == series))
&& heading.is_none_or(|heading| spond.heading == *heading);
log::trace!("{}: {:?} == {:?} => {:?}", spond.heading, spond.series_id, series, result);
result
})
{
log::debug!("{:?}", spond.responses);
for spond in sponds.iter().filter(|spond| {
let result = series
.is_none_or(|series| spond.series_id.is_some_and(|remote| remote == series))
&& heading.is_none_or(|heading| spond.heading == *heading);
log::trace!(
"{}: {:?} == {:?} => {:?}",
spond.heading,
spond.series_id,
series,
result
);
result
}) {
log::debug!("{:?}", spond.responses);
let spond = &spond;
let decline = |id: &api::MemberId| {
log::info!("remove {0}", *id);
spond.decline(*id).call(client)
};
let accept = |id: &api::MemberId| {
log::info!("accept {0}", *id);
spond.accept(*id).call(client)
};
let spond = &spond;
let decline = |id: &api::MemberId| {
log::info!("remove {0}", *id);
spond.decline(*id).call(client)
};
let accept = |id: &api::MemberId| {
log::info!("accept {0}", *id);
spond.accept(*id).call(client)
};
let mut weights = spond.series_id.and_then(|series| Weights::load(series).ok()).unwrap_or_else(Weights::default);
log::info!("{weights:?}");
let mut weights = spond
.series_id
.and_then(|series| Weights::load(series).ok())
.unwrap_or_else(Weights::default);
log::info!("{weights:?}");
let (vip, interested) = {
let mut r = (Vec::new(), Vec::new());
for id in spond.responses.accepted_ids.iter()
.chain(spond.responses.waitinglist_ids.iter()) {
(if vip.contains(id) { &mut r.0 } else { &mut r.1 }).push(*id);
}
(r.0, seed.shuffle(&r.1, |idx| weights.weight(idx))?)
};
// remove all registered participants
let results = futures::future::join_all(interested.iter().map(|id|decline(id))).await;
log::debug!("{results:?}");
// register them in order
let mut responses = None;
for id in interested.iter() {
responses = Some(accept(id).await?);
let (vip, interested) = {
let mut r = (Vec::new(), Vec::new());
for id in spond
.responses
.accepted_ids
.iter()
.chain(spond.responses.waitinglist_ids.iter())
{
(if vip.contains(id) { &mut r.0 } else { &mut r.1 }).push(*id);
}
(r.0, seed.shuffle(&r.1, |idx| weights.weight(idx))?)
};
if let Some(responses) = responses {
log::debug!("{responses:?}");
// remove all registered participants
let results = futures::future::join_all(interested.iter().map(&decline)).await;
log::debug!("{results:?}");
let reorder = |mut responses: api::Responses| async move {
// someone might have been registered right now
let mut extra = Vec::new();
loop {
log::debug!("vip: {vip:?}");
log::debug!("interested: {interested:?}");
log::debug!("extra: {extra:?}");
let reorder = responses.accepted_ids.iter()
.chain(responses.waitinglist_ids.iter())
.filter(|id| !(vip.contains(*id) || interested.contains(*id) || extra.contains(*id)))
// register them in order
let mut responses = None;
for id in interested.iter() {
responses = Some(accept(id).await?);
}
if let Some(responses) = responses {
log::debug!("{responses:?}");
let reorder = |mut responses: api::Responses| async move {
// someone might have been registered right now
let mut extra = Vec::new();
loop {
log::debug!("vip: {vip:?}");
log::debug!("interested: {interested:?}");
log::debug!("extra: {extra:?}");
let reorder = responses
.accepted_ids
.iter()
.chain(responses.waitinglist_ids.iter())
.filter(|id| {
!(vip.contains(*id)
|| interested.contains(*id)
|| extra.contains(*id))
})
.cloned()
.collect::<Vec<_>>();
if reorder.is_empty() {
let update = interested
.iter()
.filter(|id| responses.waitinglist_ids.contains(id))
.cloned()
.collect::<Vec<_>>();
if reorder.is_empty() {
let update = interested.iter()
.filter(|id| responses.waitinglist_ids.contains(id))
.cloned()
.collect::<Vec<_>>();
break Ok::<Vec<api::MemberId>, anyhow::Error>(update);
}
let futures = futures::future::join_all(reorder.iter().map(|id|decline(id))).await;
log::debug!("{futures:?}");
for id in reorder.into_iter() {
responses = accept(&id).await?;
extra.push(id);
}
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
break Ok::<Vec<api::MemberId>, anyhow::Error>(update);
}
};
let futures =
futures::future::join_all(reorder.iter().map(&decline)).await;
log::debug!("{futures:?}");
let update = reorder(responses).await?;
weights.update(&update);
} else {
weights = Weights::default();
for id in reorder.into_iter() {
responses = accept(&id).await?;
extra.push(id);
}
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
}
};
log::debug!("{weights:?}");
let update = reorder(responses).await?;
weights.update(&update);
} else {
weights = Weights::default();
};
if let Some(series) = spond.series_id {
let _ = weights.store(series)?;
}
}
log::debug!("{weights:?}");
//for member in spond.responses.accepted_ids.iter()
// .chain(spond.responses.waitinglist_ids.iter()) {
// let result = map.insert(member, 1);
// println!("{:?}: {:?}", member, result);
//}
//println!("{:?}", map);
//println!("{:?}", &spond.responses);
//let response = spond.response(member)
// .accepted(false)
// .call(&client)
// .await?;
//println!("{:?}", &response);
} else if true {
let profile = api::profile::identity(&client).await;
if let Ok(profile) = profile {
println!("profile: {:?}: {profile}", &profile.id);
if let Some(series) = spond.series_id {
weights.store(series)?;
}
} else if false {
//let query = [
// ("includeSponds", "true"),
//];
//let series = client.get_with::<_, Series>((0xCCBE049C31DA4FB691158E3FBC2DFBC8u128,), &query).await?.into_inner().0;
let now = api::util::DateTime::default();
let sponds = api::spond::search()
.include_comments(true)
.order(api::Order::Ascending)
.max(100)
.series_id(api::SeriesId::new(0x9333BDD4135E48BEAE88F1C3006A5FC0u128.into()))
.min_end_timestamp(now)
.min_start_timestamp(now)
.call(&client).await?;
for spond in sponds.iter() {
println!("{spond:?}");
//let spond = api::spond().call(&client, api::SpondId::new(0xF131CD46F80A42B9909D8E7F4018D8E1u128.into())).await?;
//println!("{spond:?}");
}
//} else if true {
//
//#[derive(Debug, serde::Deserialize, serde::Serialize)]
//struct Spond(serde_json::Value);
//
//#[derive(Debug, serde::Deserialize, serde::Serialize)]
//struct Sponds(Vec<Spond>);
//
//impl restson::RestPath<()> for Sponds {
// fn get_path(_: ()) -> std::result::Result<String, restson::Error> {
// Ok(String::from("sponds"))
// }
//}
// let now = &DateTime::default();
// log::info!("{now:?} | {}", now.to_string());
// let query = [
// ("includeComments", "true"),
// //("includeHidden", "true"),
// ("addProfileInfo", "true"),
// ("hidden", "true"),
// ("scheduled", "true"),
// ("order", "asc"),
// ("max", "20"),
// ("heading", "Schwimmtraining Donnerstag"),
// ("seriesId", "CCBE049C31DA4FB691158E3FBC2DFBC8u128"),
// ("minStartTimestamp", &now.to_string()),
// ];
//
// for spond in client.get_with::<_, api::spond::Sponds>((), &query).await?.into_inner().0 {
// //match spond.0 {
// // serde_json::Value::Object(map) => {
// // println!("{:?}", map);
// // },
// // _ => {},
// //};
// println!("{}", serde_json::to_string_pretty(&spond).unwrap());
// }
//} else {
// let request = api::sponds()
// .add_profile_info(false)
// .comments(true)
// .hidden(false)
// .scheduled(true)
// ;
// for spond in request.call(&client).await? {
// println!("{spond:?}");
// }
}
Ok(())