use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
Upload,
Delete,
Mirror,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookPayload {
pub event: EventType,
pub sha256: String,
pub size: u64,
pub pubkey: String,
pub timestamp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
pub trait WebhookNotifier: Send + Sync {
fn notify(&self, payload: WebhookPayload);
}
pub struct NoopNotifier;
impl WebhookNotifier for NoopNotifier {
fn notify(&self, _payload: WebhookPayload) {}
}
pub struct HttpNotifier {
urls: Vec<String>,
client: reqwest::Client,
}
impl HttpNotifier {
pub fn new(urls: Vec<String>) -> Self {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self { urls, client }
}
}
impl WebhookNotifier for HttpNotifier {
fn notify(&self, payload: WebhookPayload) {
for url in &self.urls {
let client = self.client.clone();
let url = url.clone();
let payload = payload.clone();
tokio::spawn(async move {
if let Err(e) = client.post(&url).json(&payload).send().await {
tracing::warn!(
webhook.url = %url,
error.message = %e,
"webhook delivery failed"
);
}
});
}
}
}
pub fn make_payload(
event: EventType,
sha256: &str,
size: u64,
pubkey: &str,
metadata: Option<serde_json::Value>,
) -> WebhookPayload {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
WebhookPayload {
event,
sha256: sha256.to_string(),
size,
pubkey: pubkey.to_string(),
timestamp,
metadata,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_payload_serde() {
let payload = make_payload(EventType::Upload, &"a".repeat(64), 1024, "pubkey", None);
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"event\":\"upload\""));
assert!(json.contains("\"size\":1024"));
let parsed: WebhookPayload = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.event, EventType::Upload);
}
#[test]
fn test_noop_notifier() {
let notifier = NoopNotifier;
let payload = make_payload(EventType::Delete, &"b".repeat(64), 0, "pk", None);
notifier.notify(payload); }
#[test]
fn test_payload_with_metadata() {
let meta = serde_json::json!({"source_url": "https://example.com/blob"});
let payload = make_payload(EventType::Mirror, &"c".repeat(64), 512, "pk", Some(meta));
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("source_url"));
}
}