use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Prefix { pub raw: String, pub nick: Option, pub user: Option, pub host: Option, } impl Prefix { fn parse(raw: &str) -> Prefix { let (nick, user, host) = if let Some((n, rest)) = raw.split_once('!') { match rest.split_once('@') { Some((u, h)) => (Some(n), Some(u), Some(h)), None => (Some(n), Some(rest), None), } } else if let Some((n, h)) = raw.split_once('@') { (Some(n), None, Some(h)) } else { (Some(raw), None, None) }; Prefix { raw: raw.to_string(), nick: nick.map(str::to_string), user: user.map(str::to_string), host: host.map(str::to_string), } } pub fn name(&self) -> &str { self.nick.as_deref().unwrap_or(&self.raw) } } #[derive(Debug, Clone)] pub struct Message { pub tags: HashMap, pub prefix: Option, pub command: String, pub params: Vec, } impl Message { pub fn parse(line: &str) -> Option { let mut rest = line.trim_end_matches(['\r', '\n']).trim_start(); if rest.is_empty() { return None; } let mut tags = HashMap::new(); if let Some(stripped) = rest.strip_prefix('@') { let (tag_str, r) = stripped.split_once(' ')?; rest = r.trim_start(); for item in tag_str.split(';').filter(|s| !s.is_empty()) { match item.split_once('=') { Some((k, v)) => tags.insert(k.to_string(), unescape_tag_value(v)), None => tags.insert(item.to_string(), String::new()), }; } } let mut prefix = None; if let Some(stripped) = rest.strip_prefix(':') { let (p, r) = stripped.split_once(' ')?; prefix = Some(Prefix::parse(p)); rest = r.trim_start(); } let (command, mut rest) = match rest.split_once(' ') { Some((c, r)) => (c, r.trim_start()), None => (rest, ""), }; if command.is_empty() { return None; } let mut params = Vec::new(); while !rest.is_empty() { if let Some(trailing) = rest.strip_prefix(':') { params.push(trailing.to_string()); break; } match rest.split_once(' ') { Some((p, r)) => { if !p.is_empty() { params.push(p.to_string()); } rest = r.trim_start(); } None => { params.push(rest.to_string()); break; } } } Some(Message { tags, prefix, command: command.to_uppercase(), params, }) } pub fn param(&self, index: usize) -> Option<&str> { self.params.get(index).map(String::as_str) } pub fn trailing(&self) -> Option<&str> { self.params.last().map(String::as_str) } pub fn nick(&self) -> Option<&str> { self.prefix.as_ref().and_then(|p| p.nick.as_deref()) } pub fn tag(&self, key: &str) -> Option<&str> { self.tags.get(key).map(String::as_str) } } fn unescape_tag_value(value: &str) -> String { let mut out = String::with_capacity(value.len()); let mut chars = value.chars(); while let Some(c) = chars.next() { if c != '\\' { out.push(c); continue; } match chars.next() { Some(':') => out.push(';'), Some('s') => out.push(' '), Some('\\') => out.push('\\'), Some('r') => out.push('\r'), Some('n') => out.push('\n'), Some(other) => out.push(other), None => {} } } out }