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