rubot/src/irc/message.rs
2026-07-29 16:24:30 +00:00

143 lines
4 KiB
Rust

use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prefix {
pub raw: String,
pub nick: Option<String>,
pub user: Option<String>,
pub host: Option<String>,
}
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<String, String>,
pub prefix: Option<Prefix>,
pub command: String,
pub params: Vec<String>,
}
impl Message {
pub fn parse(line: &str) -> Option<Message> {
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
}