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; #[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>; async fn parse(self, response: Response) -> Result>; } #[derive(Debug)] struct EventsRequest { query: std::vec::Vec<(String, String)>, } impl SpondRequest for EventsRequest { type Response = std::vec::Vec; async fn request(&self, client: &Client) -> Result> { let request = client.request(Method::GET, Spond::endpoint("sponds")) .query(&self.query); Ok(request) } async fn parse(self, response: Response) -> Result> { Ok(response.json().await?) } } impl EventsRequest { pub fn new() -> Self { Self { query: std::vec::Vec::new() } } fn add(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> { 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>(&self, info: T) -> Result> { 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(&self, info: &T) -> Result> { 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, } 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> { #[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> { Ok(response) } } #[tokio::main] async fn main() -> Result<(), Box> { 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(()) }