254 lines
6.1 KiB
Rust
254 lines
6.1 KiB
Rust
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(())
|
|
}
|