Skip to main content

rustant_core/channels/
webchat.rs

1//! WebChat channel — wraps the existing GatewayServer for browser-based chat.
2//!
3//! This adapter bridges the Channel trait to the WebSocket gateway,
4//! allowing the agent to interact with web-based clients via the same
5//! channel abstraction used for Telegram, Discord, etc.
6
7use super::{
8    Channel, ChannelCapabilities, ChannelMessage, ChannelStatus, ChannelType, MessageId,
9    StreamingMode,
10};
11use crate::error::RustantError;
12use crate::gateway::{GatewayEvent, SharedGateway};
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15use std::sync::{Arc, Mutex};
16
17/// Configuration for the WebChat channel.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct WebChatConfig {
20    pub enabled: bool,
21}
22
23impl Default for WebChatConfig {
24    fn default() -> Self {
25        Self { enabled: true }
26    }
27}
28
29/// WebChat channel backed by the gateway.
30pub struct WebChatChannel {
31    gateway: Option<SharedGateway>,
32    status: ChannelStatus,
33    name: String,
34    outbox: Arc<Mutex<Vec<ChannelMessage>>>,
35}
36
37impl WebChatChannel {
38    pub fn new() -> Self {
39        Self {
40            gateway: None,
41            status: ChannelStatus::Disconnected,
42            name: "webchat".to_string(),
43            outbox: Arc::new(Mutex::new(Vec::new())),
44        }
45    }
46
47    /// Attach a shared gateway for real WebSocket connectivity.
48    pub fn with_gateway(mut self, gw: SharedGateway) -> Self {
49        self.gateway = Some(gw);
50        self
51    }
52
53    pub fn with_name(mut self, name: impl Into<String>) -> Self {
54        self.name = name.into();
55        self
56    }
57}
58
59impl Default for WebChatChannel {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65#[async_trait]
66impl Channel for WebChatChannel {
67    fn name(&self) -> &str {
68        &self.name
69    }
70
71    fn channel_type(&self) -> ChannelType {
72        ChannelType::WebChat
73    }
74
75    async fn connect(&mut self) -> Result<(), RustantError> {
76        self.status = ChannelStatus::Connected;
77        Ok(())
78    }
79
80    async fn disconnect(&mut self) -> Result<(), RustantError> {
81        self.status = ChannelStatus::Disconnected;
82        Ok(())
83    }
84
85    async fn send_message(&self, msg: ChannelMessage) -> Result<MessageId, RustantError> {
86        let id = msg.id.clone();
87
88        // If we have a gateway, broadcast the message as an event
89        if let Some(ref gw) = self.gateway {
90            let text = msg.content.as_text().unwrap_or("").to_string();
91            let gw = gw.lock().await;
92            gw.broadcast(GatewayEvent::AssistantMessage { content: text });
93        }
94
95        // Also store in outbox for testing
96        self.outbox.lock().unwrap().push(msg);
97        Ok(id)
98    }
99
100    async fn receive_messages(&self) -> Result<Vec<ChannelMessage>, RustantError> {
101        // In a real implementation, this would read from the gateway's incoming queue.
102        // For now, return an empty list — messages come in via the WS handler.
103        Ok(Vec::new())
104    }
105
106    fn status(&self) -> ChannelStatus {
107        self.status
108    }
109
110    fn capabilities(&self) -> ChannelCapabilities {
111        ChannelCapabilities {
112            supports_threads: false,
113            supports_reactions: false,
114            supports_files: true,
115            supports_voice: false,
116            supports_video: false,
117            max_message_length: None,
118            supports_editing: false,
119            supports_deletion: false,
120        }
121    }
122
123    fn streaming_mode(&self) -> StreamingMode {
124        StreamingMode::WebSocket
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::channels::ChannelUser;
132
133    #[tokio::test]
134    async fn test_webchat_lifecycle() {
135        let mut ch = WebChatChannel::new();
136        assert_eq!(ch.status(), ChannelStatus::Disconnected);
137
138        ch.connect().await.unwrap();
139        assert!(ch.is_connected());
140
141        ch.disconnect().await.unwrap();
142        assert!(!ch.is_connected());
143    }
144
145    #[tokio::test]
146    async fn test_webchat_send_message() {
147        let ch = WebChatChannel::new();
148        let outbox = ch.outbox.clone();
149
150        let sender = ChannelUser::new("web-user", ChannelType::WebChat);
151        let msg =
152            ChannelMessage::text(ChannelType::WebChat, "session-1", sender, "Hello from web!");
153        let id = ch.send_message(msg).await.unwrap();
154        assert!(!id.0.is_empty());
155
156        let outbox = outbox.lock().unwrap();
157        assert_eq!(outbox.len(), 1);
158        assert_eq!(outbox[0].content.as_text(), Some("Hello from web!"));
159    }
160
161    #[tokio::test]
162    async fn test_webchat_receive_empty() {
163        let ch = WebChatChannel::new();
164        let msgs = ch.receive_messages().await.unwrap();
165        assert!(msgs.is_empty());
166    }
167
168    #[test]
169    fn test_webchat_capabilities() {
170        let ch = WebChatChannel::new();
171        let caps = ch.capabilities();
172        assert!(!caps.supports_threads);
173        assert!(caps.supports_files);
174        assert!(caps.max_message_length.is_none());
175    }
176
177    #[test]
178    fn test_webchat_streaming_mode() {
179        let ch = WebChatChannel::new();
180        assert_eq!(ch.streaming_mode(), StreamingMode::WebSocket);
181    }
182}