Skip to main content

rustyclaw_core/messengers/
webhook.rs

1//! Webhook messenger - POST messages to a URL.
2
3use super::{Message, Messenger, SendOptions};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde::Serialize;
7
8/// Simple webhook messenger that POSTs messages to a URL
9pub struct WebhookMessenger {
10    name: String,
11    webhook_url: String,
12    connected: bool,
13    http: reqwest::Client,
14}
15
16impl WebhookMessenger {
17    pub fn new(name: String, webhook_url: String) -> Self {
18        Self {
19            name,
20            webhook_url,
21            connected: false,
22            http: reqwest::Client::new(),
23        }
24    }
25}
26
27#[derive(Serialize)]
28struct WebhookPayload<'a> {
29    content: &'a str,
30    recipient: &'a str,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    reply_to: Option<&'a str>,
33}
34
35#[async_trait]
36impl Messenger for WebhookMessenger {
37    fn name(&self) -> &str {
38        &self.name
39    }
40
41    fn messenger_type(&self) -> &str {
42        "webhook"
43    }
44
45    async fn initialize(&mut self) -> Result<()> {
46        self.connected = true;
47        Ok(())
48    }
49
50    async fn send_message(&self, recipient: &str, content: &str) -> Result<String> {
51        let payload = WebhookPayload {
52            content,
53            recipient,
54            reply_to: None,
55        };
56
57        let resp = self
58            .http
59            .post(&self.webhook_url)
60            .json(&payload)
61            .send()
62            .await?;
63
64        if resp.status().is_success() {
65            Ok(format!("webhook-{}", chrono::Utc::now().timestamp_millis()))
66        } else {
67            anyhow::bail!("Webhook returned {}", resp.status())
68        }
69    }
70
71    async fn send_message_with_options(&self, opts: SendOptions<'_>) -> Result<String> {
72        let payload = WebhookPayload {
73            content: opts.content,
74            recipient: opts.recipient,
75            reply_to: opts.reply_to,
76        };
77
78        let resp = self
79            .http
80            .post(&self.webhook_url)
81            .json(&payload)
82            .send()
83            .await?;
84
85        if resp.status().is_success() {
86            Ok(format!("webhook-{}", chrono::Utc::now().timestamp_millis()))
87        } else {
88            anyhow::bail!("Webhook returned {}", resp.status())
89        }
90    }
91
92    async fn receive_messages(&self) -> Result<Vec<Message>> {
93        // Webhooks are typically outbound-only
94        Ok(Vec::new())
95    }
96
97    fn is_connected(&self) -> bool {
98        self.connected
99    }
100
101    async fn disconnect(&mut self) -> Result<()> {
102        self.connected = false;
103        Ok(())
104    }
105}