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

2004
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
use restson::{Error, Response, RestClient, RestPath};
use serde::{Deserialize, Serialize};
use restson::{RestClient, RestPath, Error, Response};
#[derive(Debug, Copy, Clone, Serialize)]
struct Email<'a> {
@ -37,8 +37,8 @@ impl<'a> RestPath<()> for Token<'a> {
}
pub mod token {
use serde::{Serialize, Deserialize};
use crate::util::DateTime;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -114,14 +114,25 @@ where
Ok(tokens.into_inner())
}
pub fn email<'a>(client: &'a RestClient, email: &'a str, password: &'a str) -> impl Future<Output=Result<Tokens, Error>> + 'a {
pub fn email<'a>(
client: &'a RestClient,
email: &'a str,
password: &'a str,
) -> impl Future<Output = Result<Tokens, Error>> + 'a {
authenticate(client, Email { email, password })
}
pub fn phone<'a>(client: &'a RestClient, phone: &'a str, password: &'a str) -> impl Future<Output=Result<Tokens, Error>> + 'a {
pub fn phone<'a>(
client: &'a RestClient,
phone: &'a str,
password: &'a str,
) -> impl Future<Output = Result<Tokens, Error>> + 'a {
authenticate(client, Phone { phone, password })
}
pub fn token<'a>(client: &'a RestClient, token: &'a str) -> impl Future<Output=Result<Tokens, Error>> + 'a {
pub fn token<'a>(
client: &'a RestClient,
token: &'a str,
) -> impl Future<Output = Result<Tokens, Error>> + 'a {
authenticate(client, Token { token })
}

View file

@ -58,16 +58,13 @@ impl<'a> Query<'a> {
}
#[derive(Serialize)]
#[derive(Default)]
pub enum Order {
#[default]
Ascending,
Descending,
}
impl Default for Order {
fn default() -> Self {
Self::Ascending
}
}
impl std::fmt::Display for Order {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View file

@ -37,8 +37,5 @@ pub async fn identity(client: &RestClient) -> Result<Profile, Error> {
}
pub async fn with_id(client: &RestClient, id: Id) -> Result<Profile, Error> {
Ok(client
.get_with::<_, Profile>(id, &[])
.await?
.into_inner())
Ok(client.get_with::<_, Profile>(id, &[]).await?.into_inner())
}

View file

@ -64,13 +64,10 @@ impl Spond {
#[builder]
pub fn response(
&self,
#[builder(start_fn)]
member: MemberId,
#[builder(finish_fn)]
client: &RestClient,
#[builder(default = true)]
accepted: bool,
) -> impl Future<Output=Result<Responses, Error>> {
#[builder(start_fn)] member: MemberId,
#[builder(finish_fn)] client: &RestClient,
#[builder(default = true)] accepted: bool,
) -> impl Future<Output = Result<Responses, Error>> {
response(self.id)
.member(member)
.accepted(accepted)
@ -78,27 +75,21 @@ impl Spond {
}
#[builder]
pub fn accept(&self,
#[builder(start_fn)]
member: MemberId,
#[builder(finish_fn)]
client: &RestClient,
) -> impl Future<Output=Result<Responses, Error>> {
self.response(member)
.accepted(true)
.call(client)
pub fn accept(
&self,
#[builder(start_fn)] member: MemberId,
#[builder(finish_fn)] client: &RestClient,
) -> impl Future<Output = Result<Responses, Error>> {
self.response(member).accepted(true).call(client)
}
#[builder]
pub fn decline(&self,
#[builder(start_fn)]
member: MemberId,
#[builder(finish_fn)]
client: &RestClient,
) -> impl Future<Output=Result<Responses, Error>> {
self.response(member)
.accepted(false)
.call(client)
pub fn decline(
&self,
#[builder(start_fn)] member: MemberId,
#[builder(finish_fn)] client: &RestClient,
) -> impl Future<Output = Result<Responses, Error>> {
self.response(member).accepted(false).call(client)
}
}
@ -198,11 +189,8 @@ pub fn decline(
#[builder(start_fn)] spond: Id,
#[builder(finish_fn)] client: &RestClient,
member: MemberId,
) -> impl std::future::Future<Output=Result<Responses, Error>> {
response(spond)
.member(member)
.accepted(false)
.call(client)
) -> impl std::future::Future<Output = Result<Responses, Error>> {
response(spond).member(member).accepted(false).call(client)
}
#[bon::builder]
@ -210,11 +198,8 @@ pub fn accept(
#[builder(start_fn)] spond: Id,
#[builder(finish_fn)] client: &RestClient,
member: MemberId,
) -> impl std::future::Future<Output=Result<Responses, Error>> {
response(spond)
.member(member)
.accepted(true)
.call(client)
) -> impl std::future::Future<Output = Result<Responses, Error>> {
response(spond).member(member).accepted(true).call(client)
}
#[bon::builder]
@ -236,7 +221,8 @@ pub async fn response(
}
}
let request = Request{accepted};
let response: restson::Response<Responses> = client.put_capture((spond, member), &request).await?;
let request = Request { accepted };
let response: restson::Response<Responses> =
client.put_capture((spond, member), &request).await?;
Ok(response.into_inner())
}

View file

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize, Serializer};
#[derive(Debug, Copy, Clone, Deserialize)]
#[serde(transparent)]
pub struct DateTime(chrono::DateTime<chrono::Utc>);
impl Serialize for DateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl Default for DateTime {
fn default() -> Self {
chrono::Utc::now().into()
}
}
impl std::fmt::Display for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.0.to_rfc3339_opts(chrono::SecondsFormat::Secs, true,)
)
}
}
impl From<chrono::DateTime<chrono::Utc>> for DateTime {
fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
Self(dt)
}
}

View file

@ -0,0 +1,5 @@
mod datetime;
pub use datetime::DateTime;
mod timestamp;
pub use timestamp::Timestamp;

View file

@ -0,0 +1,51 @@
use chrono::TimeZone;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; // for timestamp_millis_opt
#[derive(Debug, Clone, Copy)]
pub struct Timestamp(chrono::DateTime<chrono::Utc>);
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(self.0.timestamp_millis())
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let millis = i64::deserialize(deserializer)?;
let dt = chrono::Utc
.timestamp_millis_opt(millis)
.single()
.ok_or_else(|| serde::de::Error::custom("invalid timestamp"))?;
Ok(Timestamp(dt))
}
}
impl Default for Timestamp {
fn default() -> Self {
chrono::Utc::now().into()
}
}
impl std::fmt::Display for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.0.to_rfc3339_opts(chrono::SecondsFormat::Secs, true,)
)
}
}
impl<I: Into<chrono::DateTime<chrono::Utc>>> From<I> for Timestamp {
fn from(dt: I) -> Self {
Self(dt.into())
}
}

114
api/src/util/id.rs Normal file
View file

@ -0,0 +1,114 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::hash::{Hash, Hasher};
fn short_type_name(full: &str) -> &str {
let generic = full.find('<').unwrap_or(full.len());
let last_colon = full[..generic].rfind("::").map_or(0, |idx| idx + 2);
&full[last_colon..]
}
pub trait Type: Sized {
type Type: fmt::Display + Copy;
fn name() -> &'static str {
short_type_name(std::any::type_name::<Self>())
}
}
pub struct Id<T: Type>(T::Type);
impl<T: Type> AsRef<T::Type> for Id<T> {
fn as_ref(&self) -> &T::Type {
&self.0
}
}
impl<T: Type> Copy for Id<T> {}
impl<T: Type> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Type> Id<T> {
pub fn new(src: T::Type) -> Self {
Self(src)
}
pub async fn load(
&self,
client: &restson::RestClient,
) -> Result<T, restson::Error>
where
T: restson::RestPath<Id<T>>,
T: serde::de::DeserializeOwned,
{ Ok(client.get_with::<_, T>(*self, &[]).await?.into_inner()) }
}
impl<T: Type> fmt::Debug for Id<T>
where
T::Type: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ID<{}:{}>", T::name(), self.0)
}
}
impl<T: Type> fmt::Display for Id<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<T> Serialize for Id<T>
where
T: Type,
T::Type: Serialize,
{
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
impl<'de, T> Deserialize<'de> for Id<T>
where
T: Type,
T::Type: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Self(T::Type::deserialize(deserializer)?))
}
}
impl<T> PartialEq for Id<T>
where
T: Type,
T::Type: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T> Eq for Id<T>
where
T: Type,
T::Type: Eq,
{
}
impl<T> Hash for Id<T>
where
T: Type,
T::Type: Hash,
{
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state)
}
}

11
api/src/util/mod.rs Normal file
View file

@ -0,0 +1,11 @@
pub mod chrono;
pub use chrono::{DateTime, Timestamp};
pub mod x128;
pub use x128::X128;
pub mod id;
pub use id::Id;
pub mod visibility;
pub use visibility::Visibility;

View file

@ -0,0 +1,36 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Visibility {
Invitees,
Unknown(String),
}
impl Serialize for Visibility {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = match self {
Visibility::Invitees => "INVITEES",
Visibility::Unknown(other) => other,
};
serializer.serialize_str(s)
}
}
impl<'de> Deserialize<'de> for Visibility {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Borrow when possible
let s = <&str>::deserialize(deserializer)?;
Ok(match s {
"INVITEES" => Visibility::Invitees,
other => Visibility::Unknown(other.to_owned()),
})
}
}

84
api/src/util/x128.rs Normal file
View file

@ -0,0 +1,84 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
/// 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<T: Into<u128>> From<T> for X128 {
fn from(src: T) -> Self {
Self::new(src.into())
}
}
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)
}
}

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

View file

@ -47,8 +47,8 @@ fn extract_args(inputs: &Punctuated<syn::FnArg, Comma>, class: Class, default: C
for arg in inputs.iter().skip(1) {
if let syn::FnArg::Typed(pat_type) = arg {
//if pat_type.attrs.iter().any(|a| a.path().is_ident(path)) {
if Class::classify(&pat_type, default) == class {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
if Class::classify(pat_type, default) == class
&& let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
idents.push(pat_ident.ident.clone());
types.push((*pat_type.ty).clone());
let meta = pat_type.attrs.iter()
@ -58,7 +58,6 @@ fn extract_args(inputs: &Punctuated<syn::FnArg, Comma>, class: Class, default: C
};
attrs.push(meta);
}
}
}
}
(idents, types, attrs)