143 lines
4 KiB
Rust
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
|
|
}
|