initial commit
This commit is contained in:
commit
e788d55cca
6 changed files with 2479 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
2166
Cargo.lock
generated
Normal file
2166
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "spond"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5", features = [ "derive" ] }
|
||||||
|
reqwest = { version = "0.12", features = [ "json", "cookies" ] }
|
||||||
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
serde = "1.0"
|
||||||
|
env_logger = "0.10"
|
||||||
|
log = "0.4"
|
||||||
|
chrono = { version = "0.4", features = [ "serde" ] }
|
||||||
|
serde_json = "1.0"
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1758427187,
|
||||||
|
"narHash": "sha256-pHpxZ/IyCwoTQPtFIAG2QaxuSm8jWzrzBGjwQZIttJc=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "554be6495561ff07b6c724047bdd7e0716aa7b46",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
17
flake.nix
Normal file
17
flake.nix
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
description = "spond bot";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }: {
|
||||||
|
devShells = builtins.mapAttrs (_: pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
cargo
|
||||||
|
openssl.dev
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}) nixpkgs.legacyPackages;
|
||||||
|
};
|
||||||
|
}
|
||||||
254
src/main.rs
Normal file
254
src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue