From d4600788017389d7dec70bd323ba8858f05f10aa Mon Sep 17 00:00:00 2001 From: Sculas Date: Wed, 25 Jan 2023 22:54:55 +0100 Subject: [PATCH] feat: init --- .gitignore | 3 +++ .vercelignore | 1 + README.md | 10 +++++++++ api/Cargo.toml | 21 ++++++++++++++++++ api/github.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ api/types.rs | 24 +++++++++++++++++++++ api/utils.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ vercel.json | 7 ++++++ 8 files changed, 180 insertions(+) create mode 100644 .gitignore create mode 100644 .vercelignore create mode 100644 README.md create mode 100644 api/Cargo.toml create mode 100644 api/github.rs create mode 100644 api/types.rs create mode 100644 api/utils.rs create mode 100644 vercel.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc945c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/**/target +.vercel +Cargo.lock \ No newline at end of file diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..20efc89 --- /dev/null +++ b/.vercelignore @@ -0,0 +1 @@ +/**/target diff --git a/README.md b/README.md new file mode 100644 index 0000000..776f765 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# gh-discord-transformer + +A small serverless function to transform GitHub sponsorship webhook events into Discord webhook events. +**Requires Vercel.** + +## Environment Variables + +- `GITHUB_SECRET`: github webhook secret +- `DISCORD_WEBHOOK_URL`: discord webhook url +- `DISCORD_WEBHOOK_TID`: optional discord webhook thread id diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..892444e --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sponsor-shields" +version = "0.1.0" +authors = ["Sculas "] +edition = "2021" + +[dependencies] +http = "0.2" +reqwest = { version = "0.11", features = ["json"] } +serde = "1" +serde_json = "1.0" +hmac-sha256 = "1.1.6" +hex = "0.4.3" + +[dependencies.vercel_lambda] +git = "https://github.com/hanabi1224/rust" +branch = "update_lambda_runtime_deps" + +[[bin]] +name = "github" +path = "./github.rs" diff --git a/api/github.rs b/api/github.rs new file mode 100644 index 0000000..82f16da --- /dev/null +++ b/api/github.rs @@ -0,0 +1,56 @@ +use hmac_sha256::HMAC; +use http::StatusCode; +use serde_json::from_slice; +use std::env::var; +use std::result::Result as StdResult; +use utils::{Error, Result}; +use vercel_lambda::{lambda_async, IntoResponse, Request, Response}; + +mod types; +mod utils; + +async fn handler(req: Request) -> Result { + verify_sig(&req)?; + if !is_ping_event(&req) { + let event: types::GithubEvent = from_slice(req.body()).map_err(|_| Error("bad-json"))?; + if event.action == "created" { + utils::notify_discord( + &event.sponsorship.sponsor.login, + &event.sponsorship.sponsor.html_url, + event.sponsorship.tier.monthly_price_in_dollars, + ) + .await?; + } + } else { + println!("ping event"); + } + Ok(Response::builder().status(StatusCode::OK).body(()).unwrap()) +} + +fn is_ping_event(req: &Request) -> bool { + req.headers().get("X-GitHub-Event").map(|s| s.as_bytes()) == Some(b"ping") +} + +fn verify_sig(req: &Request) -> Result<()> { + let req_sig = match req + .headers() + .get("X-Hub-Signature-256") + .map(|s| s.as_bytes()) + { + Some(sig) => sig, + None => return Err(Error("no-sig")), + }; + let secret = var("GITHUB_SECRET").map_err(|_| Error("no-secret"))?; + let sig = format!("sha256={}", hex::encode(HMAC::mac(req.body(), &secret))); + // I know this is unsafe, but I don't really care for this project + if sig.as_bytes() != req_sig { + return Err(Error("bad-sig")); + } + Ok(()) +} + +// Start the runtime with the handler +fn main() -> StdResult<(), Box> { + wrap_handler!(handler => wrapped_handler); + Ok(lambda_async!(wrapped_handler)) +} diff --git a/api/types.rs b/api/types.rs new file mode 100644 index 0000000..af3c380 --- /dev/null +++ b/api/types.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct GithubEvent { + pub action: String, + pub sponsorship: Sponsorship, +} + +#[derive(Debug, Deserialize)] +pub struct Sponsorship { + pub sponsor: Sponsor, + pub tier: Tier, +} + +#[derive(Debug, Deserialize)] +pub struct Sponsor { + pub login: String, + pub html_url: String, +} + +#[derive(Debug, Deserialize)] +pub struct Tier { + pub monthly_price_in_dollars: f64, +} diff --git a/api/utils.rs b/api/utils.rs new file mode 100644 index 0000000..077583d --- /dev/null +++ b/api/utils.rs @@ -0,0 +1,58 @@ +pub struct Error(pub &'static str); + +pub type Result = std::result::Result; + +pub async fn notify_discord(username: &str, profile_url: &str, usd_donated: f64) -> Result<()> { + let webhook_url = std::env::var("DISCORD_WEBHOOK_URL").map_err(|_| Error("no-webhook-url"))?; + let thread_id = std::env::var("DISCORD_WEBHOOK_TID").ok(); + let client = reqwest::Client::builder() + .build() + .map_err(|_| Error("bad-client"))?; + let mut req = client.post(&webhook_url); + if let Some(thread_id) = thread_id { + req = req.query(&[("thread_id", thread_id)]); + } + let res = req + .json(&serde_json::json!({ + "embeds": [ + { + "title": "New Sponsor!", + "description": format!("[{}]({}) just donated ${}!", username, profile_url, usd_donated), + "color": 0x00ff00, + "footer": { + "text": "Sponsorship Notifications" + } + } + ], + "allowed_mentions": { + "parse": [] + } + })) + .send() + .await + .map_err(|_| Error("discord-bad-request"))?; + if res.status() != http::StatusCode::NO_CONTENT { + return Err(Error("bad-discord-status")); + } + Ok(()) +} + +#[macro_export] +macro_rules! wrap_handler { + ($handler:ident => $wrapped_handler:ident) => { + async fn $wrapped_handler( + req: vercel_lambda::Request, + ) -> std::result::Result< + vercel_lambda::Response, + vercel_lambda::error::VercelError, + > { + match $handler(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Ok(vercel_lambda::Response::builder() + .status(http::StatusCode::BAD_REQUEST) + .body(err.0.into()) + .unwrap()), + } + } + }; +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..84ce906 --- /dev/null +++ b/vercel.json @@ -0,0 +1,7 @@ +{ + "functions": { + "api/github.rs": { + "runtime": "vercel-rust@3.1.2" + } + } +} \ No newline at end of file