autopulse_service/settings/webhooks/
json.rs1use super::{transport, WebhookBatch};
2use chrono::Utc;
3use serde::{Deserialize, Serialize};
4
5#[derive(Serialize, Clone)]
6struct JsonWebhookEvent {
7 event: String,
8 action: String,
9 trigger: Option<String>,
10 files: Vec<String>,
11 file_count: usize,
12 timestamp: String,
13}
14
15#[derive(Serialize, Clone)]
16struct JsonWebhookPayload {
17 events: Vec<JsonWebhookEvent>,
18}
19
20#[derive(Serialize, Deserialize, Clone)]
21pub struct JsonWebhook {
22 pub url: String,
24}
25
26impl JsonWebhook {
27 fn generate_payload(&self, batch: &WebhookBatch) -> JsonWebhookPayload {
28 let timestamp = Utc::now().to_rfc3339();
29 let events = batch
30 .iter()
31 .map(|(event, trigger, files)| JsonWebhookEvent {
32 event: event.key().to_string(),
33 action: event.action().to_string(),
34 trigger: trigger.clone(),
35 files: files.clone(),
36 file_count: files.len(),
37 timestamp: timestamp.clone(),
38 })
39 .collect();
40
41 JsonWebhookPayload { events }
42 }
43
44 pub async fn send(
45 &self,
46 batch: &WebhookBatch,
47 retries: u8,
48 timeout_secs: u64,
49 ) -> anyhow::Result<()> {
50 let payload = self.generate_payload(batch);
51
52 transport::shared_sender(std::time::Duration::from_secs(timeout_secs))?
53 .send_json(&self.url, &[payload], retries)
54 .await
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::settings::webhooks::EventType;
62
63 #[test]
64 fn generate_payload_exposes_stable_event_fields() {
65 let webhook = JsonWebhook {
66 url: "https://example.com/webhooks/json".to_string(),
67 };
68 let batch = vec![
69 (
70 EventType::New,
71 Some("sonarr".to_string()),
72 vec!["/media/tv/show-01.mkv".to_string()],
73 ),
74 (
75 EventType::HashMismatch,
76 None,
77 vec!["/media/tv/show-02.mkv".to_string()],
78 ),
79 ];
80
81 let payload = serde_json::to_value(webhook.generate_payload(&batch)).unwrap();
82 let events = payload
83 .get("events")
84 .and_then(serde_json::Value::as_array)
85 .unwrap();
86
87 assert_eq!(events.len(), 2);
88 assert_eq!(events[0]["event"], "new");
89 assert_eq!(events[0]["action"], "added");
90 assert_eq!(events[0]["trigger"], "sonarr");
91 assert_eq!(
92 events[0]["files"],
93 serde_json::json!(["/media/tv/show-01.mkv"])
94 );
95 assert_eq!(events[0]["file_count"], 1);
96 assert!(
97 chrono::DateTime::parse_from_rfc3339(events[0]["timestamp"].as_str().unwrap()).is_ok()
98 );
99 assert_eq!(events[1]["event"], "hash_mismatch");
100 assert!(events[1]["trigger"].is_null());
101 }
102}