working
This commit is contained in:
parent
e69bcfc23d
commit
b09e8eafbb
16 changed files with 2531 additions and 258 deletions
2004
Cargo.lock
generated
Normal file
2004
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
33
api/src/util/chrono/datetime.rs
Normal file
33
api/src/util/chrono/datetime.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
5
api/src/util/chrono/mod.rs
Normal file
5
api/src/util/chrono/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod datetime;
|
||||
pub use datetime::DateTime;
|
||||
|
||||
mod timestamp;
|
||||
pub use timestamp::Timestamp;
|
||||
51
api/src/util/chrono/timestamp.rs
Normal file
51
api/src/util/chrono/timestamp.rs
Normal 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
114
api/src/util/id.rs
Normal 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
11
api/src/util/mod.rs
Normal 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;
|
||||
36
api/src/util/visibility.rs
Normal file
36
api/src/util/visibility.rs
Normal 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
84
api/src/util/x128.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)?))
|
||||
|
|
|
|||
303
cli/src/main.rs
303
cli/src/main.rs
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue