diff --git a/backend/Rocket.example.toml b/backend/Rocket.example.toml new file mode 100644 index 0000000..47c3426 --- /dev/null +++ b/backend/Rocket.example.toml @@ -0,0 +1,42 @@ +# This is all the config you need for this service +[global] +address = "0.0.0.0" +switchcounter_webhook = "..." +discord_webhook = "" + +# I don't know how to generate these without using Go, sorry. These are hex-encoded +# ED25519 public/private keys. +[global.paseto] +public = "" +private = "" + +# fetch this from PluralKit directly +[global.pluralkit] +system_id = "" +token = "" + +# fetch these from Pluralkit directly +[global.pluralkit.mappings] + +# why the fuck does twitter need 4 goddamn API keys holy shit +[global.twitter] +consumer_token = "..." +consumer_secret = "..." +api_key = "..." +api_secret = "..." + +# This MIGHT work with pleroma, godspeed. Fetch these values from +# the API. +[global.mastodon] +instance = "..." +app_id = "..." +app_secret = "..." +token = "..." +account = "..." + +# Get these values by creating an oAuth app +[global.reddit] +app_id = "..." +app_secret = "..." +username = "..." +password = "..." \ No newline at end of file diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs new file mode 100644 index 0000000..6319ccd --- /dev/null +++ b/backend/src/api/mod.rs @@ -0,0 +1,78 @@ +use crate::{models, paseto, schema, web, MainDatabase}; +use diesel::prelude::*; +use rocket::{ + data::{self, FromDataSimple}, + http::Status, + request::Request, + response::Responder, + Data, + Outcome::*, + Response, +}; +use rocket_contrib::json::Json; +use std::io::Read; + +pub mod switch; + +#[get("/members")] +#[instrument(skip(conn), err)] +pub fn get_members(tok: paseto::Token, conn: MainDatabase) -> Result>> { + use schema::members; + let results = members::table + .load::(&*conn) + .map_err(Error::Database)?; + + Ok(Json(results)) +} + +#[get("/token/info")] +pub fn token_info(tok: paseto::Token) -> Json { + Json(tok) +} + +#[derive(Debug)] +pub struct StringBody(String); + +impl StringBody { + fn unwrap(self) -> String { + self.0 + } +} + +impl FromDataSimple for StringBody { + type Error = String; + + fn from_data(_req: &Request, data: Data) -> data::Outcome { + let mut contents = String::new(); + + if let Err(e) = data.open().take(256).read_to_string(&mut contents) { + return Failure((Status::InternalServerError, format!("{:?}", e))); + } + + Success(StringBody(contents)) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("internal database error: {0}")] + Database(#[from] diesel::result::Error), + + #[error("not found")] + NotFound, + + #[error("web API interop error: {0}")] + Web(#[from] web::Error), +} + +pub type Result = std::result::Result; + +impl<'a> Responder<'a> for Error { + fn respond_to(self, _: &Request) -> ::std::result::Result, Status> { + error!("{}", self); + match self { + Error::NotFound => Err(Status::NotFound), + _ => Err(Status::InternalServerError), + } + } +} diff --git a/backend/src/api.rs b/backend/src/api/switch.rs similarity index 67% rename from backend/src/api.rs rename to backend/src/api/switch.rs index ded2015..60d98c2 100644 --- a/backend/src/api.rs +++ b/backend/src/api/switch.rs @@ -1,34 +1,10 @@ +use super::{Error, Result, StringBody}; use crate::{models, paseto, schema, web, MainDatabase}; use chrono::prelude::*; use diesel::prelude::*; -use rocket::{ - data::{self, FromDataSimple}, - http::Status, - request::Request, - response::Responder, - Data, - Outcome::*, - Response, State, -}; +use rocket::State; use rocket_contrib::json::Json; use rusty_ulid::generate_ulid_string; -use std::io::Read; - -#[get("/members")] -#[instrument(skip(conn), err)] -pub fn get_members(tok: paseto::Token, conn: MainDatabase) -> Result>> { - use schema::members; - let results = members::table - .load::(&*conn) - .map_err(Error::Database)?; - - Ok(Json(results)) -} - -#[get("/token/info")] -pub fn token_info(tok: paseto::Token) -> Json { - Json(tok) -} #[derive(serde::Serialize)] pub struct FrontChange { @@ -41,7 +17,7 @@ pub struct FrontChange { #[get("/switches?&")] #[instrument(skip(conn), err)] -pub fn get_switches( +pub fn list( conn: MainDatabase, count: Option, page: Option, @@ -49,7 +25,7 @@ pub fn get_switches( ) -> Result>> { use schema::{members, switches}; - let count = count.unwrap_or(50); + let count = count.unwrap_or(30); let page = page.unwrap_or(0); let count = if count < 100 { count } else { 100 }; @@ -79,7 +55,7 @@ pub fn get_switches( #[get("/switches/current")] #[instrument(skip(conn), err)] -pub fn get_current_front(conn: MainDatabase, tok: paseto::Token) -> Result> { +pub fn current_front(conn: MainDatabase, tok: paseto::Token) -> Result> { use schema::{members, switches}; let mut front: Vec<(models::Switch, models::Member)> = switches::table @@ -103,7 +79,7 @@ pub fn get_current_front(conn: MainDatabase, tok: paseto::Token) -> Result, @@ -166,11 +142,7 @@ pub fn make_switch( #[get("/switches/")] #[instrument(skip(conn), err)] -pub fn get_switch( - tok: paseto::Token, - conn: MainDatabase, - switch_id: String, -) -> Result> { +pub fn get(tok: paseto::Token, conn: MainDatabase, switch_id: String) -> Result> { use schema::{members, switches::dsl::switches}; let (switch, member): (models::Switch, models::Member) = switches @@ -187,50 +159,3 @@ pub fn get_switch( ended_at: switch.ended_at, })) } - -#[derive(Debug)] -pub struct StringBody(String); - -impl StringBody { - fn unwrap(self) -> String { - self.0 - } -} - -impl FromDataSimple for StringBody { - type Error = String; - - fn from_data(_req: &Request, data: Data) -> data::Outcome { - let mut contents = String::new(); - - if let Err(e) = data.open().take(256).read_to_string(&mut contents) { - return Failure((Status::InternalServerError, format!("{:?}", e))); - } - - Success(StringBody(contents)) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("internal database error: {0}")] - Database(#[from] diesel::result::Error), - - #[error("not found")] - NotFound, - - #[error("web API interop error: {0}")] - Web(#[from] web::Error), -} - -pub type Result = std::result::Result; - -impl<'a> Responder<'a> for Error { - fn respond_to(self, _: &Request) -> ::std::result::Result, Status> { - error!("{}", self); - match self { - Error::NotFound => Err(Status::NotFound), - _ => Err(Status::InternalServerError), - } - } -} diff --git a/backend/src/main.rs b/backend/src/main.rs index 964e83a..95cebc9 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -45,16 +45,17 @@ fn main() -> Result<()> { .attach(paseto::ed25519_keypair()) .attach(web::pluralkit::Client::fairing()) .attach(web::switchcounter::Client::fairing()) + .attach(web::discord_webhook::Client::fairing()) .mount("/metrics", prometheus) .mount("/", routes![botinfo]) .mount( "/api", routes![ + api::switch::current_front, + api::switch::get, + api::switch::list, + api::switch::switch, api::get_members, - api::get_switches, - api::get_switch, - api::get_current_front, - api::make_switch, api::token_info ], ) diff --git a/backend/src/web/discord_webhook.rs b/backend/src/web/discord_webhook.rs new file mode 100644 index 0000000..007b55c --- /dev/null +++ b/backend/src/web/discord_webhook.rs @@ -0,0 +1,58 @@ +use super::{Error, Result}; +use rocket::fairing::AdHoc; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Clone, Debug, Deserialize)] +struct AllowedMentions { + parse: Vec, +} + +#[derive(Serialize, Clone, Debug, Deserialize)] +pub struct Body { + content: String, + allowed_mentions: AllowedMentions, +} + +impl Body { + pub fn new(body: T) -> Body + where + T: Into, + { + Body { + content: body.into(), + allowed_mentions: AllowedMentions { parse: vec![] }, + } + } +} + +pub struct Client { + webhook_url: String, +} + +impl Client { + pub fn new(webhook_url: String) -> Self { + Client { + webhook_url: webhook_url, + } + } + + pub fn fairing() -> AdHoc { + AdHoc::on_attach("Switch Counter API", |rocket| { + let webhook_url = rocket.config().get_string("discord_webhook").unwrap(); + Ok(rocket.manage(Client::new(webhook_url))) + }) + } + + pub fn send(&self, body: String) -> Result<()> { + let resp = ureq::post(&self.webhook_url).send_json(serde_json::to_value(Body::new(body))?); + + if resp.ok() { + Ok(()) + } else { + Err(match resp.synthetic_error() { + Some(why) => Error::UReq(why.to_string()), + None => Error::HttpStatus(resp.status()), + }) + } + } +} diff --git a/backend/src/web/mod.rs b/backend/src/web/mod.rs index 273c218..38d6112 100644 --- a/backend/src/web/mod.rs +++ b/backend/src/web/mod.rs @@ -1,3 +1,4 @@ +pub mod discord_webhook; pub mod pluralkit; pub mod switchcounter;