chat_system/servers/
irc.rs1use crate::message::{Message, MessageType};
8use crate::server::{ChatListener, MessageHandler};
9use anyhow::Result;
10use async_trait::async_trait;
11use std::sync::Arc;
12use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
13use tokio::net::TcpListener;
14
15const CTCP_DELIM: char = '\x01';
17
18pub struct IrcListener {
40 address: String,
41 server_name: String,
42 shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
43}
44
45impl IrcListener {
46 pub fn new(address: impl Into<String>) -> Self {
49 Self {
50 address: address.into(),
51 server_name: "localhost".to_string(),
52 shutdown_tx: None,
53 }
54 }
55
56 pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
58 self.server_name = name.into();
59 self
60 }
61}
62
63async fn send_welcome(
66 writer: &mut (impl tokio::io::AsyncWrite + Unpin),
67 server_name: &str,
68 nick: &str,
69) -> Result<()> {
70 let lines = [
71 format!(":{server_name} 001 {nick} :Welcome to the Internet Relay Network {nick}\r\n"),
72 format!(":{server_name} 002 {nick} :Your host is {server_name}, running chat-system\r\n"),
73 format!(":{server_name} 003 {nick} :This server was created with chat-system\r\n"),
74 format!(":{server_name} 004 {nick} {server_name} chat-system o o\r\n"),
75 ];
76 for line in &lines {
77 writer.write_all(line.as_bytes()).await?;
78 }
79 Ok(())
80}
81
82#[allow(dead_code)] pub(super) async fn handle_connection(
89 stream: impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
90 handler: MessageHandler,
91) -> Result<()> {
92 handle_connection_with_name(stream, handler, "localhost").await
93}
94
95pub(super) async fn handle_connection_with_name(
98 stream: impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
99 handler: MessageHandler,
100 server_name: &str,
101) -> Result<()> {
102 let (reader, mut writer) = tokio::io::split(stream);
103 let mut lines = BufReader::new(reader).lines();
104 let mut nick = String::new();
105 let mut user_seen = false;
106 let mut registered = false;
107
108 while let Some(line) = lines.next_line().await? {
109 let line = line.trim().to_string();
110 if line.is_empty() {
111 continue;
112 }
113
114 if let Some(rest) = line.strip_prefix("NICK ") {
115 nick = rest.trim().to_string();
116 } else if line.starts_with("USER ") {
117 user_seen = true;
118 } else if line.starts_with("PING ") {
119 let token = line.trim_start_matches("PING ");
120 writer
121 .write_all(format!("PONG {}\r\n", token).as_bytes())
122 .await?;
123 } else if let Some(rest) = line.strip_prefix("PRIVMSG ") {
124 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
125 if parts.len() == 2 {
126 let target = parts[0];
127 let content = parts[1].trim_start_matches(':');
128
129 let (msg_content, msg_type) =
131 if content.starts_with(CTCP_DELIM) && content.ends_with(CTCP_DELIM) {
132 let inner = content
133 .trim_start_matches(CTCP_DELIM)
134 .trim_end_matches(CTCP_DELIM);
135 if let Some(action_text) = inner.strip_prefix("ACTION ") {
136 (action_text.to_string(), MessageType::Action)
137 } else {
138 continue;
140 }
141 } else {
142 (content.to_string(), MessageType::Text)
143 };
144
145 let msg = Message {
146 id: format!("irc-{}", chrono::Utc::now().timestamp_millis()),
147 sender: nick.clone(),
148 content: msg_content,
149 timestamp: chrono::Utc::now().timestamp(),
150 channel: Some(target.to_string()),
151 reply_to: None,
152 thread_id: None,
153 media: None,
154 is_direct: !target.starts_with('#'),
155 message_type: msg_type,
156 edited_timestamp: None,
157 reactions: None,
158 };
159 if let Ok(Some(reply)) = handler(msg).await {
160 let response = format!(
161 ":{server_name}!{server_name}@{server_name} PRIVMSG {} :{}\r\n",
162 target, reply
163 );
164 writer.write_all(response.as_bytes()).await?;
165 }
166 }
167 } else if let Some(rest) = line.strip_prefix("NOTICE ") {
168 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
170 if parts.len() == 2 {
171 let target = parts[0];
172 let content = parts[1].trim_start_matches(':');
173 let msg = Message {
174 id: format!("irc-{}", chrono::Utc::now().timestamp_millis()),
175 sender: nick.clone(),
176 content: content.to_string(),
177 timestamp: chrono::Utc::now().timestamp(),
178 channel: Some(target.to_string()),
179 reply_to: None,
180 thread_id: None,
181 media: None,
182 is_direct: !target.starts_with('#'),
183 message_type: MessageType::System,
184 edited_timestamp: None,
185 reactions: None,
186 };
187 let _ = handler(msg).await;
189 }
190 } else if let Some(rest) = line.strip_prefix("JOIN ") {
191 let channel = rest.trim().trim_start_matches(':');
192 writer
194 .write_all(format!(":{nick}!{nick}@{server_name} JOIN {channel}\r\n").as_bytes())
195 .await?;
196 } else if line.starts_with("PART ") || line.starts_with("TOPIC ") {
197 } else if line == "QUIT" || line.starts_with("QUIT ") {
199 break;
200 }
201
202 if !registered && !nick.is_empty() && user_seen {
203 send_welcome(&mut writer, server_name, &nick).await?;
204 registered = true;
205 }
206 }
207 Ok(())
208}
209
210#[async_trait]
211impl ChatListener for IrcListener {
212 fn address(&self) -> &str {
213 &self.address
214 }
215
216 fn protocol(&self) -> &str {
217 "irc"
218 }
219
220 async fn start(
221 &mut self,
222 handler: MessageHandler,
223 alive: tokio::sync::mpsc::Sender<()>,
224 ) -> Result<()> {
225 let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
226 let listener = TcpListener::bind(&self.address).await?;
227 self.address = listener.local_addr()?.to_string();
229 tracing::info!(address = %self.address, "IRC listener bound");
230 self.shutdown_tx = Some(shutdown_tx);
231 let server_name = self.server_name.clone();
232
233 tokio::spawn(async move {
234 let _alive = alive;
237
238 loop {
239 tokio::select! {
240 result = listener.accept() => {
241 match result {
242 Ok((stream, peer)) => {
243 tracing::debug!(%peer, "IRC listener: new connection");
244 let h = Arc::clone(&handler);
245 let sn = server_name.clone();
246 tokio::spawn(async move {
247 if let Err(e) = handle_connection_with_name(stream, h, &sn).await {
248 tracing::warn!("IRC connection error: {e}");
249 }
250 });
251 }
252 Err(e) => {
253 tracing::warn!("IRC listener accept error: {e}");
254 break;
255 }
256 }
257 }
258 _ = shutdown_rx.changed() => {
259 if *shutdown_rx.borrow() {
260 break;
261 }
262 }
263 }
264 }
265 });
266
267 Ok(())
268 }
269
270 async fn shutdown(&mut self) -> Result<()> {
271 if let Some(tx) = &self.shutdown_tx {
272 let _ = tx.send(true);
273 }
274 Ok(())
275 }
276}