Skip to main content

rustyclaw_core/messengers/
teams.rs

1//! Microsoft Teams messenger using incoming webhooks.
2//!
3//! Uses Teams incoming webhook connectors for sending messages.
4//! For bidirectional messaging, a Teams Bot Framework registration is required.
5
6use super::{Message, Messenger, SendOptions};
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9
10/// Microsoft Teams messenger using webhooks / Bot Framework.
11pub struct TeamsMessenger {
12    name: String,
13    /// Incoming webhook URL for sending messages.
14    webhook_url: Option<String>,
15    /// Bot Framework app ID (for bidirectional messaging).
16    app_id: Option<String>,
17    /// Bot Framework app password.
18    app_password: Option<String>,
19    connected: bool,
20    http: reqwest::Client,
21}
22
23impl TeamsMessenger {
24    pub fn new(name: String) -> Self {
25        Self {
26            name,
27            webhook_url: None,
28            app_id: None,
29            app_password: None,
30            connected: false,
31            http: reqwest::Client::new(),
32        }
33    }
34
35    /// Set the incoming webhook URL.
36    pub fn with_webhook_url(mut self, url: String) -> Self {
37        self.webhook_url = Some(url);
38        self
39    }
40
41    /// Set Bot Framework credentials.
42    pub fn with_bot_framework(mut self, app_id: String, app_password: String) -> Self {
43        self.app_id = Some(app_id);
44        self.app_password = Some(app_password);
45        self
46    }
47}
48
49#[async_trait]
50impl Messenger for TeamsMessenger {
51    fn name(&self) -> &str {
52        &self.name
53    }
54
55    fn messenger_type(&self) -> &str {
56        "teams"
57    }
58
59    async fn initialize(&mut self) -> Result<()> {
60        if self.webhook_url.is_none() && self.app_id.is_none() {
61            anyhow::bail!(
62                "Teams requires either 'webhook_url' (incoming webhook) or \
63                 'app_id'+'app_password' (Bot Framework)"
64            );
65        }
66        self.connected = true;
67        tracing::info!(
68            has_webhook = self.webhook_url.is_some(),
69            has_bot_framework = self.app_id.is_some(),
70            "Teams initialized"
71        );
72        Ok(())
73    }
74
75    async fn send_message(&self, _channel: &str, content: &str) -> Result<String> {
76        if let Some(ref webhook_url) = self.webhook_url {
77            // Teams Adaptive Card or simple text
78            let payload = serde_json::json!({
79                "@type": "MessageCard",
80                "@context": "http://schema.org/extensions",
81                "text": content
82            });
83
84            let resp = self
85                .http
86                .post(webhook_url)
87                .json(&payload)
88                .send()
89                .await
90                .context("Teams webhook POST failed")?;
91
92            if resp.status().is_success() {
93                return Ok(format!("teams-{}", chrono::Utc::now().timestamp_millis()));
94            }
95            anyhow::bail!("Teams webhook returned {}", resp.status());
96        }
97
98        // Bot Framework send requires conversation reference — not yet implemented
99        anyhow::bail!(
100            "Teams Bot Framework send not yet implemented. Use webhook_url or the claw-me-maybe skill."
101        )
102    }
103
104    async fn send_message_with_options(&self, opts: SendOptions<'_>) -> Result<String> {
105        self.send_message(opts.recipient, opts.content).await
106    }
107
108    async fn receive_messages(&self) -> Result<Vec<Message>> {
109        // Receiving requires Bot Framework with an HTTP endpoint
110        // or Graph API subscription. Return empty for now.
111        Ok(Vec::new())
112    }
113
114    fn is_connected(&self) -> bool {
115        self.connected
116    }
117
118    async fn disconnect(&mut self) -> Result<()> {
119        self.connected = false;
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_teams_creation() {
130        let m = TeamsMessenger::new("test".to_string())
131            .with_webhook_url("https://outlook.office.com/webhook/xxx".to_string());
132        assert_eq!(m.name(), "test");
133        assert_eq!(m.messenger_type(), "teams");
134        assert!(!m.is_connected());
135    }
136
137    #[test]
138    fn test_with_bot_framework() {
139        let m = TeamsMessenger::new("test".to_string())
140            .with_bot_framework("app-id".to_string(), "app-pass".to_string());
141        assert_eq!(m.app_id, Some("app-id".to_string()));
142        assert_eq!(m.app_password, Some("app-pass".to_string()));
143    }
144}