webhook delivery
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub async fn fire_event(state: &Arc<AppState>, event: &str, payload: serde_json::Value) {
|
||||
let dbi = state.db.lock().await;
|
||||
let hooks = crate::db::get_enabled_webhooks(&dbi.db, event);
|
||||
drop(dbi);
|
||||
|
||||
for (id, url, secret, format) in hooks {
|
||||
if crate::security::is_internal_url(&url) {
|
||||
tracing::warn!("skipping webhook to internal URL: {}", url);
|
||||
continue;
|
||||
}
|
||||
let body = match format.as_str() {
|
||||
"discord" => format_discord(event, &payload),
|
||||
"slack" => format_slack(event, &payload),
|
||||
_ => serde_json::json!({ "event": event, "data": payload }).to_string(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(crate::security::safe_redirect_policy())
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
let mut req = client.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Cruciverb-Event", event);
|
||||
|
||||
if let Some(ref sec) = secret {
|
||||
use sha2::{Sha256, Digest};
|
||||
let sig = hex::encode(Sha256::digest(format!("{}{}", body, sec).as_bytes()));
|
||||
req = req.header("X-Cruciverb-Signature", sig);
|
||||
}
|
||||
|
||||
let state_clone = state.clone();
|
||||
let id_clone = id.clone();
|
||||
tokio::spawn(async move {
|
||||
match req.body(body).send().await {
|
||||
Ok(res) if res.status().is_success() => {
|
||||
if crate::security::is_internal_url(res.url().as_str()) {
|
||||
tracing::warn!("skipping webhook delivery to internal URL after redirect: {}", res.url());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let dbi = state_clone.db.lock().await;
|
||||
crate::db::record_webhook_failure(&dbi.db, &id_clone);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn format_discord(event: &str, payload: &serde_json::Value) -> String {
|
||||
let title = match event {
|
||||
"daily_puzzle.published" => "New daily puzzle",
|
||||
"puzzle.completed" => "Puzzle completed",
|
||||
"achievement.unlocked" => "Achievement unlocked",
|
||||
_ => event,
|
||||
};
|
||||
let desc = payload.get("description").and_then(|v| v.as_str()).unwrap_or("");
|
||||
serde_json::json!({
|
||||
"embeds": [{
|
||||
"title": title,
|
||||
"description": desc,
|
||||
"color": 13848143,
|
||||
}]
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
fn format_slack(event: &str, payload: &serde_json::Value) -> String {
|
||||
let desc = payload.get("description").and_then(|v| v.as_str()).unwrap_or(event);
|
||||
serde_json::json!({
|
||||
"text": desc,
|
||||
"blocks": [{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": format!("*{}*\n{}", event, desc) }
|
||||
}]
|
||||
}).to_string()
|
||||
}
|
||||
Reference in New Issue
Block a user