Skip to main content

rustant_core/channels/
irc.rs

1//! IRC channel via raw TCP/TLS.
2//!
3//! Connects to an IRC server using a persistent TCP connection.
4//! In tests, a trait abstraction provides mock implementations.
5
6use super::{
7    Channel, ChannelCapabilities, ChannelMessage, ChannelStatus, ChannelType, ChannelUser,
8    MessageId, StreamingMode,
9};
10use crate::error::{ChannelError, RustantError};
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13
14/// Configuration for an IRC channel.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct IrcConfig {
17    pub enabled: bool,
18    pub server: String,
19    pub port: u16,
20    pub nick: String,
21    pub channels: Vec<String>,
22    pub use_tls: bool,
23}
24
25impl Default for IrcConfig {
26    fn default() -> Self {
27        Self {
28            enabled: false,
29            server: String::new(),
30            port: 6667,
31            nick: "rustant".into(),
32            channels: Vec::new(),
33            use_tls: false,
34        }
35    }
36}
37
38/// Trait for IRC connection interactions.
39#[async_trait]
40pub trait IrcConnection: Send + Sync {
41    async fn connect(&self) -> Result<(), String>;
42    async fn disconnect(&self) -> Result<(), String>;
43    async fn send_privmsg(&self, target: &str, text: &str) -> Result<(), String>;
44    async fn receive(&self) -> Result<Vec<IrcMessage>, String>;
45}
46
47/// An incoming IRC message.
48#[derive(Debug, Clone)]
49pub struct IrcMessage {
50    pub nick: String,
51    pub channel: String,
52    pub text: String,
53}
54
55/// IRC channel.
56pub struct IrcChannel {
57    config: IrcConfig,
58    status: ChannelStatus,
59    connection: Box<dyn IrcConnection>,
60    name: String,
61}
62
63impl IrcChannel {
64    pub fn new(config: IrcConfig, connection: Box<dyn IrcConnection>) -> Self {
65        Self {
66            config,
67            status: ChannelStatus::Disconnected,
68            connection,
69            name: "irc".to_string(),
70        }
71    }
72
73    pub fn with_name(mut self, name: impl Into<String>) -> Self {
74        self.name = name.into();
75        self
76    }
77}
78
79#[async_trait]
80impl Channel for IrcChannel {
81    fn name(&self) -> &str {
82        &self.name
83    }
84
85    fn channel_type(&self) -> ChannelType {
86        ChannelType::Irc
87    }
88
89    async fn connect(&mut self) -> Result<(), RustantError> {
90        if self.config.server.is_empty() {
91            return Err(RustantError::Channel(ChannelError::ConnectionFailed {
92                name: self.name.clone(),
93                message: "No server configured".into(),
94            }));
95        }
96        self.connection.connect().await.map_err(|e| {
97            RustantError::Channel(ChannelError::ConnectionFailed {
98                name: self.name.clone(),
99                message: e,
100            })
101        })?;
102        self.status = ChannelStatus::Connected;
103        Ok(())
104    }
105
106    async fn disconnect(&mut self) -> Result<(), RustantError> {
107        let _ = self.connection.disconnect().await;
108        self.status = ChannelStatus::Disconnected;
109        Ok(())
110    }
111
112    async fn send_message(&self, msg: ChannelMessage) -> Result<MessageId, RustantError> {
113        if self.status != ChannelStatus::Connected {
114            return Err(RustantError::Channel(ChannelError::NotConnected {
115                name: self.name.clone(),
116            }));
117        }
118        let text = msg.content.as_text().unwrap_or("");
119        self.connection
120            .send_privmsg(&msg.channel_id, text)
121            .await
122            .map_err(|e| {
123                RustantError::Channel(ChannelError::SendFailed {
124                    name: self.name.clone(),
125                    message: e,
126                })
127            })?;
128        Ok(MessageId::random())
129    }
130
131    async fn receive_messages(&self) -> Result<Vec<ChannelMessage>, RustantError> {
132        let incoming = self.connection.receive().await.map_err(|e| {
133            RustantError::Channel(ChannelError::ConnectionFailed {
134                name: self.name.clone(),
135                message: e,
136            })
137        })?;
138
139        let messages = incoming
140            .into_iter()
141            .map(|m| {
142                let sender = ChannelUser::new(&m.nick, ChannelType::Irc);
143                ChannelMessage::text(ChannelType::Irc, &m.channel, sender, &m.text)
144            })
145            .collect();
146
147        Ok(messages)
148    }
149
150    fn status(&self) -> ChannelStatus {
151        self.status
152    }
153
154    fn capabilities(&self) -> ChannelCapabilities {
155        ChannelCapabilities {
156            supports_threads: false,
157            supports_reactions: false,
158            supports_files: false,
159            supports_voice: false,
160            supports_video: false,
161            max_message_length: Some(512),
162            supports_editing: false,
163            supports_deletion: false,
164        }
165    }
166
167    fn streaming_mode(&self) -> StreamingMode {
168        StreamingMode::WebSocket
169    }
170}
171
172/// Real IRC connection using tokio TCP.
173pub struct RealIrcConnection {
174    server: String,
175    port: u16,
176    nick: String,
177    channels: Vec<String>,
178    writer: tokio::sync::Mutex<Option<tokio::io::WriteHalf<tokio::net::TcpStream>>>,
179}
180
181impl RealIrcConnection {
182    pub fn new(server: String, port: u16, nick: String, channels: Vec<String>) -> Self {
183        Self {
184            server,
185            port,
186            nick,
187            channels,
188            writer: tokio::sync::Mutex::new(None),
189        }
190    }
191}
192
193#[async_trait]
194impl IrcConnection for RealIrcConnection {
195    async fn connect(&self) -> Result<(), String> {
196        use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
197
198        let addr = format!("{}:{}", self.server, self.port);
199        let stream = tokio::net::TcpStream::connect(&addr)
200            .await
201            .map_err(|e| format!("TCP connect error: {e}"))?;
202
203        let (reader, mut writer) = tokio::io::split(stream);
204
205        // Send NICK and USER registration
206        let nick_cmd = format!("NICK {}\r\n", self.nick);
207        let user_cmd = format!("USER {} 0 * :{}\r\n", self.nick, self.nick);
208        writer
209            .write_all(nick_cmd.as_bytes())
210            .await
211            .map_err(|e| format!("Write error: {e}"))?;
212        writer
213            .write_all(user_cmd.as_bytes())
214            .await
215            .map_err(|e| format!("Write error: {e}"))?;
216
217        // Join channels
218        for ch in &self.channels {
219            let join_cmd = format!("JOIN {}\r\n", ch);
220            writer
221                .write_all(join_cmd.as_bytes())
222                .await
223                .map_err(|e| format!("Write error: {e}"))?;
224        }
225
226        // Store writer for later use
227        *self.writer.lock().await = Some(writer);
228
229        // Spawn a background task to handle incoming lines
230        let mut buf_reader = tokio::io::BufReader::new(reader);
231        tokio::spawn(async move {
232            let mut line = String::new();
233            loop {
234                line.clear();
235                match buf_reader.read_line(&mut line).await {
236                    Ok(0) | Err(_) => break,
237                    Ok(_) => {}
238                }
239            }
240        });
241
242        Ok(())
243    }
244
245    async fn disconnect(&self) -> Result<(), String> {
246        use tokio::io::AsyncWriteExt;
247
248        if let Some(ref mut writer) = *self.writer.lock().await {
249            let _ = writer.write_all(b"QUIT :Leaving\r\n").await;
250        }
251        *self.writer.lock().await = None;
252        Ok(())
253    }
254
255    async fn send_privmsg(&self, target: &str, text: &str) -> Result<(), String> {
256        use tokio::io::AsyncWriteExt;
257
258        let mut guard = self.writer.lock().await;
259        let writer = guard.as_mut().ok_or_else(|| "Not connected".to_string())?;
260
261        let cmd = format!("PRIVMSG {} :{}\r\n", target, text);
262        writer
263            .write_all(cmd.as_bytes())
264            .await
265            .map_err(|e| format!("Write error: {e}"))?;
266
267        Ok(())
268    }
269
270    async fn receive(&self) -> Result<Vec<IrcMessage>, String> {
271        // Messages are received asynchronously through the reader task.
272        // A full implementation would use a shared message buffer.
273        Ok(vec![])
274    }
275}
276
277/// Create an IRC channel with a real TCP connection.
278pub fn create_irc_channel(config: IrcConfig) -> IrcChannel {
279    let conn = RealIrcConnection::new(
280        config.server.clone(),
281        config.port,
282        config.nick.clone(),
283        config.channels.clone(),
284    );
285    IrcChannel::new(config, Box::new(conn))
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    struct MockIrcConnection;
293
294    #[async_trait]
295    impl IrcConnection for MockIrcConnection {
296        async fn connect(&self) -> Result<(), String> {
297            Ok(())
298        }
299        async fn disconnect(&self) -> Result<(), String> {
300            Ok(())
301        }
302        async fn send_privmsg(&self, _target: &str, _text: &str) -> Result<(), String> {
303            Ok(())
304        }
305        async fn receive(&self) -> Result<Vec<IrcMessage>, String> {
306            Ok(vec![])
307        }
308    }
309
310    #[test]
311    fn test_irc_channel_creation() {
312        let ch = IrcChannel::new(IrcConfig::default(), Box::new(MockIrcConnection));
313        assert_eq!(ch.name(), "irc");
314        assert_eq!(ch.channel_type(), ChannelType::Irc);
315    }
316
317    #[test]
318    fn test_irc_capabilities() {
319        let ch = IrcChannel::new(IrcConfig::default(), Box::new(MockIrcConnection));
320        let caps = ch.capabilities();
321        assert!(!caps.supports_threads);
322        assert!(!caps.supports_files);
323        assert_eq!(caps.max_message_length, Some(512));
324    }
325
326    #[test]
327    fn test_irc_streaming_mode() {
328        let ch = IrcChannel::new(IrcConfig::default(), Box::new(MockIrcConnection));
329        assert_eq!(ch.streaming_mode(), StreamingMode::WebSocket);
330    }
331
332    #[test]
333    fn test_irc_status_disconnected() {
334        let ch = IrcChannel::new(IrcConfig::default(), Box::new(MockIrcConnection));
335        assert_eq!(ch.status(), ChannelStatus::Disconnected);
336    }
337
338    #[tokio::test]
339    async fn test_irc_send_without_connect() {
340        let ch = IrcChannel::new(IrcConfig::default(), Box::new(MockIrcConnection));
341        let sender = ChannelUser::new("nick", ChannelType::Irc);
342        let msg = ChannelMessage::text(ChannelType::Irc, "#test", sender, "hi");
343        assert!(ch.send_message(msg).await.is_err());
344    }
345}