webhook delivery

This commit is contained in:
2025-11-12 09:03:41 +02:00
parent 048b56493b
commit a13b0e2309
+78
View File
@@ -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()
}