autopulse_service/settings/webhooks/
json.rs

1use 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    /// Webhook URL
23    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}