1use log::info;
2use std::io::{self, BufRead, BufReader, BufWriter, Write};
3use std::net::TcpStream;
4use std::sync::{Arc, Mutex};
5use std::thread::{self, JoinHandle};
6
7use crate::{Message, Parser};
8
9#[derive(PartialEq)]
10pub enum IRCEvent {
11 Message(Message),
12 Users(Vec<String>),
13 MessageOfTheDay(Vec<String>),
14 Raw(String),
15}
16
17impl std::fmt::Debug for IRCEvent {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 IRCEvent::Message(msg) => write!(f, "IRCEvent::Message(cmd: {})", msg.command),
21 IRCEvent::Raw(s) => write!(f, "IRCEvent::Raw({})", s),
22 IRCEvent::Users(users) => write!(f, "IRCEvent::Users({})", users.join(", ")),
23 IRCEvent::MessageOfTheDay(motd) => {
24 write!(f, "IRCEvent::MessageOfTheDay({})", motd.join("\n"))
25 }
26 }
27 }
28}
29
30pub struct IRCClient {
31 nickname: String,
32 server: String,
33 port: u16,
34 channel: String,
35 reader: Option<BufReader<TcpStream>>,
36 writer: Option<Arc<Mutex<BufWriter<TcpStream>>>>,
37}
38
39impl IRCClient {
40 pub fn connect(
41 nickname: impl Into<String>,
42 server: impl Into<String>,
43 port: u16,
44 ) -> io::Result<Self> {
45 let mut client = Self::new(nickname, server, port);
46 client.connect_inner()?;
47 Ok(client)
48 }
49
50 fn new(nickname: impl Into<String>, server: impl Into<String>, port: u16) -> Self {
51 Self {
52 nickname: nickname.into(),
53 server: server.into(),
54 port,
55 channel: "#testchannel".to_string(),
56 reader: None,
57 writer: None,
58 }
59 }
60
61 fn connect_inner(&mut self) -> io::Result<()> {
62 let stream = TcpStream::connect((self.server.as_str(), self.port))?;
63 let reader = BufReader::new(stream.try_clone()?);
64 let writer = Arc::new(Mutex::new(BufWriter::new(stream)));
65
66 self.reader = Some(reader);
67 self.writer = Some(writer);
68
69 self.send_line(&format!("NICK {}", self.nickname))?;
70 self.send_line(&format!("USER {} 0 * :{}", self.nickname, self.nickname))?;
71 self.send_line(&format!("JOIN {}", self.channel))?;
72
73 Ok(())
74 }
75
76 pub fn quit(&mut self) -> io::Result<()> {
77 if self.writer.is_none() {
78 return Ok(());
79 }
80
81 self.send_line(&format!("PART {} :Goodbye!", self.channel))?;
82 info!("Sent PART command for channel {}", self.channel);
83 self.send_line("QUIT :Client closed")?;
84 info!("Sent QUIT command");
85
86 Ok(())
87 }
88
89 pub fn whois(&mut self, nickname: impl AsRef<str>) -> io::Result<()> {
90 let nickname = nickname.as_ref().trim();
91 if nickname.is_empty() {
92 return Ok(());
93 }
94
95 info!("Sending WHOIS command for nickname: {}", nickname);
96 self.send_line(&format!("WHOIS {}", nickname))
97 }
98
99 pub fn send_message(&mut self, message: impl AsRef<str>) -> io::Result<()> {
100 let message = message.as_ref().trim();
101 if message.is_empty() {
102 return Ok(());
103 }
104
105 info!(
106 "Sending PRIVMSG command to channel {}: {}",
107 self.channel, message
108 );
109 self.send_line(&format!("PRIVMSG {} :{}", self.channel, message))
110 }
111
112 pub fn start_listening<F>(&mut self, mut message_handler: F) -> io::Result<JoinHandle<()>>
113 where
114 F: FnMut(IRCEvent) -> io::Result<()> + Send + 'static,
115 {
116 let mut reader = self.reader.take().ok_or_else(|| {
117 io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
118 })?;
119 let writer = self.writer.clone().ok_or_else(|| {
120 io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
121 })?;
122
123 Ok(thread::spawn(move || {
124 let _ = Self::listen_loop(&mut reader, writer, &mut message_handler);
125 }))
126 }
127
128 pub fn listen<F>(&mut self, mut message_handler: F) -> io::Result<()>
129 where
130 F: FnMut(IRCEvent) -> io::Result<()>,
131 {
132 let writer = self.writer.clone().ok_or_else(|| {
133 io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
134 })?;
135 let reader = self.reader.as_mut().ok_or_else(|| {
136 io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
137 })?;
138 Self::listen_loop(reader, writer, &mut message_handler)
139 }
140
141 fn listen_loop<F>(
142 reader: &mut BufReader<TcpStream>,
143 writer: Arc<Mutex<BufWriter<TcpStream>>>,
144 message_handler: &mut F,
145 ) -> io::Result<()>
146 where
147 F: FnMut(IRCEvent) -> io::Result<()>,
148 {
149 let mut message_of_the_day = Vec::new();
150 loop {
151 let mut line = String::new();
152 let read_result = reader.read_line(&mut line);
153
154 match read_result {
155 Ok(0) => break,
156 Ok(_) => {
157 info!("Received line: {}", line.trim_end());
158
159 let mut parser = Parser::new(&line);
160 let message = parser.parse_message();
161
162 match message.command.as_str() {
163 "PING" => {
164 let response = format!("PONG :{}", message.params.join(" "));
165 Self::send_line_with_writer(&writer, &response)?;
166 continue;
167 }
168 "353" => {
169 if let Some(names) = message.params.last() {
170 let mut users: Vec<String> = Vec::new();
171 for nick in names.split_whitespace() {
172 if !nick.is_empty() {
173 users.push(nick.to_string());
174 }
175 }
176 message_handler(IRCEvent::Users(users))?;
177 }
178 }
179 "366" => {
180 info!("End of NAMES list.");
181 }
182 "375" => {
183 message_of_the_day.clear();
184 }
185 "372" => {
186 if let Some(motd_line) = message.params.last() {
187 message_of_the_day.push(motd_line.to_string());
188 }
189 }
190 "376" => {
191 message_handler(IRCEvent::MessageOfTheDay(message_of_the_day.clone()))?;
192 }
193 _ => {
194 message_handler(IRCEvent::Message(message))?;
195 }
196 }
197 }
198 Err(_) => break,
199 }
200 }
201
202 Ok(())
203 }
204
205 fn send_line(&mut self, line: &str) -> io::Result<()> {
206 let writer = self.writer.as_ref().ok_or_else(|| {
207 io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
208 })?;
209 Self::send_line_with_writer(writer, line)
210 }
211
212 fn send_line_with_writer(
213 writer: &Arc<Mutex<BufWriter<TcpStream>>>,
214 line: &str,
215 ) -> io::Result<()> {
216 let mut writer = writer
217 .lock()
218 .map_err(|_| io::Error::other("Writer lock poisoned"))?;
219
220 writer.write_all(line.as_bytes())?;
221 writer.write_all(b"\r\n")?;
222 writer.flush()
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn quit_without_connection_is_ok() {
232 let mut client = IRCClient::new("nick", "localhost", 6667);
233
234 assert!(client.quit().is_ok());
235 }
236
237 #[test]
238 fn irc_event_debug_formats_message_variant() {
239 let event = IRCEvent::Message(Message {
240 prefix: None,
241 command: "NOTICE".to_string(),
242 params: vec!["#test".to_string(), "hello".to_string()],
243 });
244
245 assert_eq!(format!("{event:?}"), "IRCEvent::Message(cmd: NOTICE)");
246 }
247
248 #[test]
249 fn irc_event_debug_formats_users_variant() {
250 let event = IRCEvent::Users(vec!["alice".to_string(), "bob".to_string()]);
251
252 assert_eq!(format!("{event:?}"), "IRCEvent::Users(alice, bob)");
253 }
254
255 #[test]
256 fn irc_event_debug_formats_raw_variant() {
257 let event = IRCEvent::Raw("Connected".to_string());
258
259 assert_eq!(format!("{event:?}"), "IRCEvent::Raw(Connected)");
260 }
261}