feat: init

This commit is contained in:
Sculas 2023-01-25 22:54:55 +01:00
commit d460078801
No known key found for this signature in database
GPG Key ID: 1530BFF96D1EEB89
8 changed files with 180 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/**/target
.vercel
Cargo.lock

1
.vercelignore Normal file
View File

@ -0,0 +1 @@
/**/target

10
README.md Normal file
View File

@ -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

21
api/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "sponsor-shields"
version = "0.1.0"
authors = ["Sculas <contact@sculas.xyz>"]
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"

56
api/github.rs Normal file
View File

@ -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<impl IntoResponse> {
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<dyn std::error::Error>> {
wrap_handler!(handler => wrapped_handler);
Ok(lambda_async!(wrapped_handler))
}

24
api/types.rs Normal file
View File

@ -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,
}

58
api/utils.rs Normal file
View File

@ -0,0 +1,58 @@
pub struct Error(pub &'static str);
pub type Result<T> = std::result::Result<T, Error>;
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::Body>,
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()),
}
}
};
}

7
vercel.json Normal file
View File

@ -0,0 +1,7 @@
{
"functions": {
"api/github.rs": {
"runtime": "vercel-rust@3.1.2"
}
}
}