use crate::{TagValue, Tags};
use std::io::{Error, ErrorKind, Result};
#[derive(Debug, PartialEq)]
pub enum Message<'a> {
Ping,
Pong,
CapReq { req: &'a str },
CapAck { req: &'a str },
Pass { pass: &'a str },
Nick { nick: &'a str },
Join { chan: &'a str },
Part { chan: &'a str },
Privmsg {
tags: Option<Tags<'a>>,
chan: &'a str,
msg: &'a str,
},
Clearchat {
tags: Option<Tags<'a>>,
chan: &'a str,
usr: Option<&'a str>,
},
Clearmsg {
tags: Option<Tags<'a>>,
chan: &'a str,
msg: &'a str,
},
HosttargetStart {
host: &'a str,
chan: &'a str,
view: Option<u32>,
},
HosttargetEnd { host: &'a str, view: Option<u32> },
Notice {
tags: Option<Tags<'a>>,
chan: &'a str,
msg: &'a str,
},
Reconnect,
Roomstate {
tags: Option<Tags<'a>>,
chan: &'a str,
},
Usernotice {
tags: Option<Tags<'a>>,
chan: &'a str,
msg: &'a str,
},
Userstate {
tags: Option<Tags<'a>>,
chan: &'a str,
},
GlobalUserstate { tags: Option<Tags<'a>> },
}
impl<'a> Message<'a> {
pub fn parse(msg: &'a str) -> Result<Message> {
if msg.len() < 5 {
return Err(Error::new(ErrorKind::Other, "Malformed message."));
}
let buf = msg.trim();
let (tags, off) = if let Some(buf) = buf.strip_prefix('@') {
Self::parse_tags(buf)?
} else {
(None, 0)
};
let mut rest = &buf[off..];
const ENDPOINT: &str = "tmi.twitch.tv ";
if let Some(off) = rest.find(ENDPOINT) {
rest = &rest[(off + ENDPOINT.len())..];
}
if let Some(off) = rest.find(' ') {
let cmd = &rest[..off];
let body = &rest[(off + 1)..];
Self::parse_command(cmd, body, tags)
} else {
Self::parse_command(rest, "", tags)
}
}
fn parse_tags(msg: &'a str) -> Result<(Option<Tags<'a>>, usize)> {
let mut map = Tags::default();
if let Some(idx) = msg.find(" :") {
let tag = &msg[..idx];
let toks = tag.split(';');
for tok in toks {
let items = tok.split('=').collect::<Vec<_>>();
let key = items[0];
let val = items[1];
map.insert(key, TagValue::new(val));
}
Ok((Some(map), idx + 3))
} else {
Err(Error::new(ErrorKind::Other, "Parsing message tags failed."))
}
}
fn parse_command(cmd: &'a str, body: &'a str, tags: Option<Tags<'a>>) -> Result<Message<'a>> {
Ok(match cmd {
"PING" => Message::Ping,
"PONG" => Message::Pong,
"CAP" => {
let off = body
.find(" :")
.ok_or_else(|| Error::new(ErrorKind::Other, "Malformed CAP command."))?;
match &body[..off] {
"REQ" => Message::CapReq {
req: &body[(off + 2)..],
},
"* ACK" => Message::CapAck {
req: &body[(off + 2)..],
},
_ => return Err(Error::new(ErrorKind::Other, "Malformed CAP command.")),
}
}
"PASS" => Message::Pass { pass: body },
"NICK" => Message::Nick { nick: body },
"JOIN" => Message::Join { chan: &body[1..] },
"PART" => Message::Part { chan: &body[1..] },
"PRIVMSG" => {
let off = body
.find(" :")
.ok_or_else(|| Error::new(ErrorKind::Other, "Malformed PRIVMSG command."))?;
Message::Privmsg {
tags,
chan: &body[1..off],
msg: &body[(off + 2)..],
}
}
"CLEARCHAT" => {
if let Some(off) = body.find(" :") {
Message::Clearchat {
tags,
chan: &body[1..off],
usr: Some(&body[(off + 2)..]),
}
} else {
Message::Clearchat {
tags,
chan: &body[1..],
usr: None,
}
}
}
"CLEARMSG" => {
let off = body
.find(" :")
.ok_or_else(|| Error::new(ErrorKind::Other, "Malformed CLEARMSG command."))?;
Message::Clearmsg {
tags,
chan: &body[1..off],
msg: &body[(off + 2)..],
}
}
"HOSTTARGET" => {
if let Some(off) = body.find(" :-") {
if body.len() > off + 5 {
if let Ok(view) = body[(off + 4)..].parse::<u32>() {
Message::HosttargetEnd {
host: &body[1..off],
view: Some(view),
}
} else {
return Err(Error::new(
ErrorKind::Other,
"Malformed HOSTTARGET command.",
));
}
} else {
Message::HosttargetEnd {
host: &body[1..off],
view: None,
}
}
} else {
let off = body.find(" :").ok_or_else(|| {
Error::new(ErrorKind::Other, "Malformed HOSTTARGET command.")
})?;
if body.len() < off + 3 {
return Err(Error::new(
ErrorKind::Other,
"Malformed HOSTTARGET command.",
));
}
let host = &body[1..off];
let body = &body[(off + 2)..];
if let Some(off) = body.find(' ') {
if body.len() > off + 2 {
if let Ok(view) = body[(off + 1)..].parse::<u32>() {
return Ok(Message::HosttargetStart {
host,
chan: &body[..off],
view: Some(view),
});
} else {
return Err(Error::new(
ErrorKind::Other,
"Malformed HOSTTARGET command.",
));
}
} else {
Message::HosttargetStart {
host,
chan: body,
view: None,
}
}
} else {
Message::HosttargetStart {
host,
chan: body,
view: None,
}
}
}
}
"NOTICE" => {
let off = body
.find(" :")
.ok_or_else(|| Error::new(ErrorKind::Other, "Malformed NOTICE command."))?;
Message::Notice {
tags,
chan: &body[1..off],
msg: &body[(off + 2)..],
}
}
"RECONNECT" => Message::Reconnect,
"ROOMSTATE" => Message::Roomstate {
tags,
chan: &body[1..],
},
"USERNOTICE" => {
let off = body
.find(" :")
.ok_or_else(|| Error::new(ErrorKind::Other, "Malformed USERNOTICE command."))?;
Message::Usernotice {
tags,
chan: &body[1..off],
msg: &body[(off + 2)..],
}
}
"USERSTATE" => Message::Userstate {
tags,
chan: &body[1..],
},
"GLOBALUSERSTATE" => Message::GlobalUserstate { tags },
_ => {
return Err(Error::new(
ErrorKind::Other,
"Parsing message command failed.",
))
}
})
}
}