mirror of
https://github.com/revanced/gh-discord-transformer.git
synced 2025-04-29 22:04:24 +02:00
feat: init
This commit is contained in:
commit
d460078801
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/**/target
|
||||||
|
.vercel
|
||||||
|
Cargo.lock
|
1
.vercelignore
Normal file
1
.vercelignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/**/target
|
10
README.md
Normal file
10
README.md
Normal 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
21
api/Cargo.toml
Normal 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
56
api/github.rs
Normal 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
24
api/types.rs
Normal 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
58
api/utils.rs
Normal 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
7
vercel.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"functions": {
|
||||||
|
"api/github.rs": {
|
||||||
|
"runtime": "vercel-rust@3.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user