initial commit

This commit is contained in:
Jonas Rabenstein 2025-09-25 16:01:17 +02:00
commit e788d55cca
6 changed files with 2479 additions and 0 deletions

254
src/main.rs Normal file
View file

@ -0,0 +1,254 @@
use reqwest::Client;
use reqwest::ClientBuilder;
use reqwest::RequestBuilder;
use reqwest::Response;
use reqwest::Method;
use reqwest::Request;
use clap::Parser;
use serde::{Deserialize, Serialize};
use log::*;
type Timestamp = chrono::DateTime<chrono::offset::Utc>;
#[derive(Debug, Serialize, Deserialize)]
struct Login<'a> {
email: &'a str,
password: &'a str,
}
#[derive(Debug, Deserialize)]
struct Token {
token: String,
expiration: Timestamp,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Tokens {
access_token: Token,
refresh_token: Token,
}
#[derive(Parser)]
struct Cli {
email: String,
password: String,
}
struct Spond {
client: Client,
tokens: Tokens,
}
#[derive(Debug)]
enum Order {
Ascending,
Descending,
}
trait SpondRequest {
type Response;
async fn request(&self, client: &Client) -> Result<RequestBuilder, Box<dyn std::error::Error>>;
async fn parse(self, response: Response) -> Result<Self::Response, Box<dyn std::error::Error>>;
}
#[derive(Debug)]
struct EventsRequest {
query: std::vec::Vec<(String, String)>,
}
impl SpondRequest for EventsRequest {
type Response = std::vec::Vec<Event>;
async fn request(&self, client: &Client) -> Result<RequestBuilder, Box<dyn std::error::Error>> {
let request = client.request(Method::GET, Spond::endpoint("sponds"))
.query(&self.query);
Ok(request)
}
async fn parse(self, response: Response) -> Result<Self::Response, Box<dyn std::error::Error>> {
Ok(response.json().await?)
}
}
impl EventsRequest {
pub fn new() -> Self {
Self { query: std::vec::Vec::new() }
}
fn add<K: std::string::ToString, V: std::string::ToString>(self, key: K, value: V) -> Self {
let mut query = self.query;
query.push((key.to_string(), value.to_string()));
Self { query }
}
pub fn comments(self, value: bool) -> Self {
self.add("includeComments", value)
}
pub fn hidden(self, value: bool) -> Self {
self.add("includeHidden", value)
}
pub fn profile_info(self, value: bool) -> Self {
self.add("addProfileInfo", value)
}
pub fn scheduled(self, value: bool) -> Self {
self.add("scheduled", value)
}
pub fn order(self, order: Order) -> Self {
self.add("order", match order {
Order::Ascending => "asc",
Order::Descending => "dsc",
})
}
pub fn max(self, count: u64) -> Self {
self.add("max", count)
}
pub fn min_end_timestamp(self, timestamp: Timestamp) -> Self {
// TODO: make actual timestamp working
self.add("minEndTimestamp", timestamp.date_naive())
}
}
impl Spond {
fn endpoint(endpoint: &str) -> String {
format!("https://api.spond.com/core/v1/{}", endpoint)
}
pub async fn new(email: &str, password: &str) -> Result<Spond, Box<dyn std::error::Error>> {
let client = ClientBuilder::new()
.cookie_store(true)
.https_only(true)
.connection_verbose(false)
.build()?;
// get the landing page for initial set of cookies
let _ = client.get("https://spond.com/landing/login/").send().await?;
// try to log in
let login = Login { email, password };
let login = client.post(Spond::endpoint("auth2/login"))
.json(&login)
.send()
.await?;
let tokens: Tokens = login.json().await?;
let spond = Spond {
client: client,
tokens: tokens,
};
Ok(spond)
}
pub async fn send<U, T: SpondRequest::<Response=U>>(&self, info: T) -> Result<U, Box<dyn std::error::Error>> {
let req = self.request(&info).await?;
debug!("send: {req:#?}");
let res = self.client.execute(req).await?;
debug!("recv: {res:#?}");
info.parse(res).await
}
pub async fn request<T: SpondRequest>(&self, info: &T) -> Result<Request, Box<dyn std::error::Error>> {
let request = info.request(&self.client)
.await?
.bearer_auth(&self.tokens.access_token.token)
.build()?;
Ok(request)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Event {
id: String,
heading: String,
start_timestamp: Timestamp,
end_timestamp: Timestamp,
behalf_of_ids: std::vec::Vec<String>,
}
impl Event {
pub fn accept(self) -> EventReplyRequest {
self.reply(true)
}
pub fn decline(self) -> EventReplyRequest {
self.reply(false)
}
pub fn reply(self, accept: bool) -> EventReplyRequest {
EventReplyRequest {
event_id: self.id,
user_id: self.behalf_of_ids[0].clone(),
accept: accept,
}
}
}
#[derive(Debug)]
struct EventReplyRequest {
event_id: String,
user_id: String,
accept: bool,
}
impl SpondRequest for EventReplyRequest {
type Response = Response;
async fn request(&self, client: &Client) -> Result<RequestBuilder, Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct Reply {
accepted: bool,
}
let request = client.request(Method::PUT, Spond::endpoint(&format!("sponds/{0}/responses/{1}", &self.event_id, &self.user_id)))
.json(&Reply { accepted: self.accept });
Ok(request)
}
async fn parse(self, response: Response) -> Result<Self::Response, Box<dyn std::error::Error>> {
Ok(response)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
trace!("trace");
debug!("debug");
info!("info");
warn!("warn");
error!("error");
let args = Cli::parse();
let spond = Spond::new(&args.email, &args.password).await?;
let events_request = EventsRequest::new()
.comments(false)
.hidden(false)
.profile_info(false)
.scheduled(true)
.order(Order::Ascending)
.max(20)
.min_end_timestamp(chrono::Utc::now())
;
let events = spond.send(events_request).await?;
for event in events {
if event.heading == "Krafttraining Donnerstag" && event.start_timestamp.date_naive() == chrono::Utc::now().date_naive() {
let result = spond.send(event.accept()).await?;
debug!("{result:#?}");
}
}
Ok(())
}