79 lines
2.8 KiB
Rust
79 lines
2.8 KiB
Rust
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()
|
|
}
|