use std::fmt; #[derive(Debug, Clone)] pub struct IrcMessage { pub tags: Option, pub prefix: Option, pub command: String, pub params: Vec, } impl IrcMessage { pub fn parse(line: &str) -> Option { let line = line.trim_end_matches(|c| c == '\r' || c == '\n'); if line.is_empty() { return None; } let mut rest = line; let tags = if rest.starts_with('@') { let end = rest.find(' ')?; let t = rest[1..end].to_string(); rest = rest[end..].trim_start(); Some(t) } else { None }; let prefix = if rest.starts_with(':') { let end = rest.find(' ')?; let p = rest[1..end].to_string(); rest = rest[end..].trim_start(); Some(p) } else { None }; let (command, remainder) = match rest.find(' ') { Some(i) => (rest[..i].to_uppercase(), &rest[i + 1..]), None => (rest.to_uppercase(), ""), }; let mut params = Vec::new(); let mut rest = remainder; while !rest.is_empty() { if rest.starts_with(':') { params.push(rest[1..].to_string()); break; } match rest.find(' ') { Some(i) => { params.push(rest[..i].to_string()); rest = &rest[i + 1..]; } None => { params.push(rest.to_string()); break; } } } Some(IrcMessage { tags, prefix, command, params, }) } pub fn nick_from_prefix(&self) -> Option<&str> { self.prefix.as_deref().and_then(|p| { p.find('!').map(|i| &p[..i]).or(Some(p)) }) } pub fn privmsg(target: &str, text: &str) -> Self { IrcMessage { tags: None, prefix: None, command: "PRIVMSG".to_string(), params: vec![target.to_string(), text.to_string()], } } pub fn join(channel: &str) -> Self { IrcMessage { tags: None, prefix: None, command: "JOIN".to_string(), params: vec![channel.to_string()], } } pub fn part(channel: &str) -> Self { IrcMessage { tags: None, prefix: None, command: "PART".to_string(), params: vec![channel.to_string()], } } pub fn nick(nick: &str) -> Self { IrcMessage { tags: None, prefix: None, command: "NICK".to_string(), params: vec![nick.to_string()], } } pub fn pong(token: &str) -> Self { IrcMessage { tags: None, prefix: None, command: "PONG".to_string(), params: vec![token.to_string()], } } } impl fmt::Display for IrcMessage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ref tags) = self.tags { write!(f, "@{} ", tags)?; } if let Some(ref prefix) = self.prefix { write!(f, ":{} ", prefix)?; } write!(f, "{}", self.command)?; for (i, param) in self.params.iter().enumerate() { if i == self.params.len() - 1 && (param.contains(' ') || param.starts_with(':') || param.is_empty()) { write!(f, " :{}", param)?; } else { write!(f, " {}", param)?; } } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_privmsg() { let msg = IrcMessage::parse(":nick!user@host PRIVMSG #channel :hello world").unwrap(); assert_eq!(msg.prefix.as_deref(), Some("nick!user@host")); assert_eq!(msg.command, "PRIVMSG"); assert_eq!(msg.params, vec!["#channel", "hello world"]); assert_eq!(msg.nick_from_prefix(), Some("nick")); } #[test] fn parse_ping() { let msg = IrcMessage::parse("PING :server.example.com").unwrap(); assert!(msg.prefix.is_none()); assert_eq!(msg.command, "PING"); assert_eq!(msg.params, vec!["server.example.com"]); } #[test] fn parse_with_tags() { let msg = IrcMessage::parse("@time=2024-01-01 :nick!u@h PRIVMSG #ch :hi").unwrap(); assert_eq!(msg.tags.as_deref(), Some("time=2024-01-01")); assert_eq!(msg.params, vec!["#ch", "hi"]); } #[test] fn serialize_privmsg() { let msg = IrcMessage::privmsg("#test", "hello world"); assert_eq!(msg.to_string(), "PRIVMSG #test :hello world"); } #[test] fn serialize_join() { let msg = IrcMessage::join("#test"); assert_eq!(msg.to_string(), "JOIN #test"); } #[test] fn parse_empty_returns_none() { assert!(IrcMessage::parse("").is_none()); assert!(IrcMessage::parse("\r\n").is_none()); } }