autopulse_service/settings/webhooks/
mod.rs

1/// Discord - Discord Webhook
2///
3/// Sends a message to a Discord Webhook on events
4///
5/// # Example
6///
7/// ```yml
8/// webhooks:
9///   my_discord:
10///     type: discord
11///     url: "https://discord.com/api/webhooks/..."
12/// ```
13///
14/// or
15///
16/// ```yml
17/// webhooks:
18///   my_discord:
19///     type: discord
20///     avatar_url: "https://example.com/avatar.png"
21///     username: "autopulse"
22/// ```
23///
24/// See [`DiscordWebhook`] for all options
25pub mod discord;
26
27/// Hookshot - Matrix Hookshot inbound webhook
28///
29/// Sends a Matrix Hookshot-compatible JSON body with `text`, generated `html`,
30/// and an optional `username`
31///
32/// # Example
33///
34/// ```yml
35/// webhooks:
36///   my_hookshot:
37///     type: hookshot
38///     url: "https://matrix.example.com/_matrix/hookshot/webhook/..."
39/// ```
40///
41/// See [`HookshotWebhook`] for all options
42pub mod hookshot;
43
44/// JSON - generic structured webhook payload
45///
46/// Sends a stable JSON object with an `events` array for generic webhook
47/// consumers
48///
49/// # Example
50///
51/// ```yml
52/// webhooks:
53///   my_json:
54///     type: json
55///     url: "https://example.com/webhooks/autopulse"
56/// ```
57///
58/// See [`JsonWebhook`] for all options
59pub mod json;
60
61#[doc(hidden)]
62pub mod manager;
63
64#[doc(hidden)]
65pub mod transport;
66
67#[doc(hidden)]
68pub use manager::*;
69
70use discord::DiscordWebhook;
71use hookshot::HookshotWebhook;
72use json::JsonWebhook;
73use serde::{Deserialize, Serialize};
74
75#[derive(Serialize, Deserialize, Clone)]
76#[serde(tag = "type", rename_all = "lowercase")]
77pub enum Webhook {
78    Discord(DiscordWebhook),
79    Hookshot(HookshotWebhook),
80    Json(JsonWebhook),
81}
82
83impl Webhook {
84    pub async fn send(
85        &self,
86        batch: &WebhookBatch,
87        retries: u8,
88        timeout_secs: u64,
89    ) -> anyhow::Result<()> {
90        if batch.is_empty() {
91            return Ok(());
92        }
93
94        match self {
95            Self::Discord(d) => d.send(batch, retries, timeout_secs).await,
96            Self::Hookshot(h) => h.send(batch, retries, timeout_secs).await,
97            Self::Json(j) => j.send(batch, retries, timeout_secs).await,
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::{hookshot::HookshotWebhook, json::JsonWebhook, Webhook};
105
106    #[test]
107    fn deserializes_hookshot_webhook_config() {
108        let webhook = serde_json::from_value::<Webhook>(serde_json::json!({
109            "type": "hookshot",
110            "url": "https://example.com/webhooks/hookshot"
111        }));
112
113        assert!(webhook.is_ok(), "expected hookshot webhook to deserialize");
114    }
115
116    #[test]
117    fn deserializes_json_webhook_config() {
118        let webhook = serde_json::from_value::<Webhook>(serde_json::json!({
119            "type": "json",
120            "url": "https://example.com/webhooks/json"
121        }));
122
123        assert!(webhook.is_ok(), "expected json webhook to deserialize");
124    }
125
126    #[tokio::test]
127    async fn skips_sending_empty_batches() {
128        let hookshot = Webhook::Hookshot(HookshotWebhook {
129            url: "http://127.0.0.1:9/hookshot".to_string(),
130            username: None,
131        });
132        let json = Webhook::Json(JsonWebhook {
133            url: "http://127.0.0.1:9/json".to_string(),
134        });
135
136        hookshot.send(&Vec::new(), 3, 10).await.unwrap();
137        json.send(&Vec::new(), 3, 10).await.unwrap();
138    }
139}