use crate::event_handler::IncomingIrcEventError;
use std::collections::HashMap;
#[derive(Debug)]
pub enum Error {
EmptyCommand,
NoSeparator,
MissingKey,
TrailingSemicolon,
NonLastParamSpaces,
NonLastParamColon,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyCommand => write!(f, "Cannot parse command-less line!"),
Self::NoSeparator => write!(f, "Cannot split line without separator!"),
Self::MissingKey => write!(f, "Cannot parse tags with missing key!"),
Self::TrailingSemicolon => write!(f, "Cannot parse tags with trailing semicolon!"),
Self::NonLastParamSpaces => write!(f, "Non last params cannot have spaces!"),
Self::NonLastParamColon => write!(f, "Non last params cannot start with colon!"),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Source {
pub nickname: String,
pub username: String,
pub hostmask: String,
}
impl Source {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn from(nickname: String, username: String, hostmask: String) -> Self {
Self {
nickname,
username,
hostmask,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Message {
pub tags: HashMap<String, String>,
pub source: Option<Source>,
pub command: String,
params: Vec<String>,
}
#[must_use]
pub fn unescape_tag(value: &str) -> String {
value
.replace("\\\\", "\\")
.replace("\\:", ";")
.replace("\\s", " ")
.replace("\\r", "\r")
.replace("\\n", "\n")
}
fn check_params(params: &Vec<String>) -> Result<(), Error> {
for i in 0..params.len() - 1 {
if let Some(param) = params.get(i) {
if param.contains(' ') {
return Err(Error::NonLastParamSpaces);
} else if param.starts_with(':') {
return Err(Error::NonLastParamColon);
}
}
}
Ok(())
}
impl Message {
pub fn new(
tags: HashMap<String, String>,
source: Option<Source>,
command: String,
params: Vec<String>,
) -> Result<Self, Error> {
match check_params(¶ms) {
Ok(()) => Ok(Self {
tags,
source,
command,
params,
}),
Err(e) => Err(e),
}
}
#[must_use]
pub fn params(&self) -> Vec<String> {
self.params.clone()
}
pub fn build_token(message: Self) -> Result<String, Error> {
let mut tags_str = Vec::new();
let mut outs: Vec<String> = Vec::new();
if !message.tags.is_empty() {
for (key, value) in message.tags {
if value.is_empty() {
tags_str.push(key.to_string());
} else {
tags_str.push(format!("{}={}", key, unescape_tag(&value)));
}
}
outs.push(format!("@{}", tags_str.join(";")));
}
if let Some(source) = message.source {
outs.push(format!(
":{}!{}@{}",
source.nickname, source.username, source.hostmask
));
}
outs.push(message.command);
let mut params = message.params.clone();
if let Some(last) = params.pop() {
for param in ¶ms {
if param.contains(' ') {
return Err(Error::NonLastParamSpaces);
} else if param.starts_with(':') {
return Err(Error::NonLastParamColon);
}
}
outs.extend(params);
let last = if last.is_empty() || last.contains(' ') || last.starts_with(':') {
format!(":{last}")
} else {
last
};
outs.push(last);
}
Ok(outs.join(" "))
}
pub fn get_param(&self, index: usize) -> Result<&str, IncomingIrcEventError> {
self.params
.get(index)
.ok_or_else(|| IncomingIrcEventError::MissingParameter(index.to_string()))
.map(std::string::String::as_str)
}
pub fn from(mut response: String) -> Result<Self, Error> {
let mut params: Vec<String> = vec![];
let line: String;
let mut trailing: Option<String> = None;
let mut nickname: String = String::new();
let mut username: String = String::new();
let mut hostmask: String = String::new();
let mut tags: HashMap<String, String> = HashMap::new();
if response.starts_with('@') {
let mut tokens = response.splitn(2, ' ');
let tags_split: String;
if let (Some(head), Some(tail)) = (tokens.next(), tokens.next()) {
tags_split = head.to_string();
response = tail.to_string();
} else {
return Err(Error::EmptyCommand);
}
let parts = tags_split[1..].split(';');
for part in parts {
if part.is_empty() {
return Err(Error::TrailingSemicolon);
}
let (key, value) = if let Some(index) = part.find('=') {
let (key, value) = part.split_at(index);
if key.is_empty() {
return Err(Error::MissingKey);
}
(key, &value[1..])
} else {
(part, "")
};
tags.insert(key.to_string(), unescape_tag(value));
}
}
let mut tokens = response.splitn(2, " :");
if let (Some(head), Some(tail)) = (tokens.next(), tokens.next()) {
line = head.to_string();
trailing = Some(tail.to_string());
} else {
line = response;
}
for sub in line.split(' ') {
let sub = sub.to_string();
if !sub.is_empty() {
params.push(sub);
}
}
if params.is_empty() {
return Err(Error::EmptyCommand); }
if params[0].as_bytes()[0] == b':' {
params[0].remove(0);
let source: String = params[0].clone(); params.remove(0);
if params.is_empty() {
return Err(Error::EmptyCommand); }
let mut tokens = source.splitn(2, '@');
if let (Some(head), Some(tail)) = (tokens.next(), tokens.next()) {
username = head.to_string();
hostmask = tail.to_string();
} else {
username = source;
}
let mut tokens = username.splitn(2, '!');
if let (Some(head), Some(tail)) = (tokens.next(), tokens.next()) {
nickname = head.to_string();
username = tail.to_string();
} else {
nickname = username.clone();
username.clear();
}
}
let command = params[0].clone();
params.remove(0);
if let Some(trailing) = trailing {
params.push(trailing);
}
Ok(Self {
tags,
source: Some(Source {
nickname,
username,
hostmask,
}),
command,
params,
})
}
}