pub mod clearchat;
pub mod clearmsg;
pub mod globaluserstate;
pub mod join;
pub mod notice;
pub mod part;
pub mod ping;
pub mod pong;
pub mod privmsg;
pub mod reconnect;
pub mod roomstate;
pub mod usernotice;
pub mod userstate;
pub mod whisper;
use self::ServerMessageParseError::{
MalformedChannel, MalformedTagValue, MissingNickname, MissingParameter, MissingPrefix,
MissingTag, MissingTagValue,
};
use crate::message::commands::clearmsg::ClearMsgMessage;
use crate::message::commands::join::JoinMessage;
use crate::message::commands::part::PartMessage;
use crate::message::commands::ping::PingMessage;
use crate::message::commands::pong::PongMessage;
use crate::message::commands::reconnect::ReconnectMessage;
use crate::message::commands::userstate::UserStateMessage;
use crate::message::prefix::IRCPrefix;
use crate::message::twitch::{Badge, Emote, RGBColor};
use crate::message::{
AsRawIRC, ClearChatMessage, GlobalUserStateMessage, IRCMessage, NoticeMessage, PrivmsgMessage,
ReplyParent, RoomStateMessage, TwitchUserBasics, UserNoticeMessage, WhisperMessage,
};
use chrono::{DateTime, TimeZone, Utc};
use std::collections::HashSet;
use std::convert::TryFrom;
use std::ops::Range;
use std::str::FromStr;
use thiserror::Error;
#[cfg(feature = "with-serde")]
use {serde::Deserialize, serde::Serialize};
#[derive(Error, Debug, PartialEq, Eq)]
pub enum ServerMessageParseError {
#[error("Could not parse IRC message {} as ServerMessage: That command's data is not parsed by this implementation", .0.as_raw_irc())]
MismatchedCommand(Box<IRCMessage>),
#[error(
"Could not parse IRC message {msg} as ServerMessage: No tag present under key `{key}`",
msg = .0.as_raw_irc(),
key = .1,
)]
MissingTag(Box<IRCMessage>, &'static str),
#[error(
"Could not parse IRC message {msg} as ServerMessage: No tag value present under key `{key}`",
msg = .0.as_raw_irc(),
key = .1,
)]
MissingTagValue(Box<IRCMessage>, &'static str),
#[error(
"Could not parse IRC message {msg} as ServerMessage: Malformed tag value for tag `{key}`, value was `{value}`",
msg = .0.as_raw_irc(),
key = .1,
value = .2,
)]
MalformedTagValue(Box<IRCMessage>, &'static str, String),
#[error(
"Could not parse IRC message {msg} as ServerMessage: No parameter found at index {key}",
msg = .0.as_raw_irc(),
key = .1,
)]
MissingParameter(Box<IRCMessage>, usize),
#[error(
"Could not parse IRC message {msg} as ServerMessage: Malformed channel parameter (# must be present + something after it)",
msg = .0.as_raw_irc(),
)]
MalformedChannel(Box<IRCMessage>),
#[error(
"Could not parse IRC message {msg} as ServerMessage: Malformed parameter at index {idx}",
msg = .0.as_raw_irc(),
idx = .1,
)]
MalformedParameter(Box<IRCMessage>, usize),
#[error(
"Could not parse IRC message {msg} as ServerMessage: Missing prefix altogether",
msg = .0.as_raw_irc(),
)]
MissingPrefix(Box<IRCMessage>),
#[error(
"Could not parse IRC message {msg} as ServerMessage: No nickname found in prefix",
msg = .0.as_raw_irc(),
)]
MissingNickname(Box<IRCMessage>),
}
impl From<ServerMessageParseError> for IRCMessage {
fn from(msg: ServerMessageParseError) -> IRCMessage {
match msg {
ServerMessageParseError::MismatchedCommand(m)
| ServerMessageParseError::MissingTag(m, _)
| ServerMessageParseError::MissingTagValue(m, _)
| ServerMessageParseError::MalformedTagValue(m, _, _)
| ServerMessageParseError::MissingParameter(m, _)
| ServerMessageParseError::MalformedChannel(m)
| ServerMessageParseError::MalformedParameter(m, _)
| ServerMessageParseError::MissingPrefix(m)
| ServerMessageParseError::MissingNickname(m) => *m,
}
}
}
trait IRCMessageParseExt {
fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError>;
fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError>;
fn try_get_tag_value(&self, key: &'static str) -> Result<&str, ServerMessageParseError>;
fn try_get_nonempty_tag_value(
&self,
key: &'static str,
) -> Result<&str, ServerMessageParseError>;
fn try_get_optional_nonempty_tag_value(
&self,
key: &'static str,
) -> Result<Option<&str>, ServerMessageParseError>;
fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError>;
fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError>;
fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError>;
fn try_get_emotes(
&self,
tag_key: &'static str,
message_text: &str,
) -> Result<Vec<Emote>, ServerMessageParseError>;
fn try_get_emote_sets(
&self,
tag_key: &'static str,
) -> Result<HashSet<String>, ServerMessageParseError>;
fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError>;
fn try_get_color(
&self,
tag_key: &'static str,
) -> Result<Option<RGBColor>, ServerMessageParseError>;
fn try_get_number<N: FromStr>(
&self,
tag_key: &'static str,
) -> Result<N, ServerMessageParseError>;
fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError>;
fn try_get_optional_number<N: FromStr>(
&self,
tag_key: &'static str,
) -> Result<Option<N>, ServerMessageParseError>;
fn try_get_optional_bool(
&self,
tag_key: &'static str,
) -> Result<Option<bool>, ServerMessageParseError>;
fn try_get_timestamp(
&self,
tag_key: &'static str,
) -> Result<DateTime<Utc>, ServerMessageParseError>;
fn try_get_optional_reply_parent(&self)
-> Result<Option<ReplyParent>, ServerMessageParseError>;
}
impl IRCMessageParseExt for IRCMessage {
fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError> {
Ok(self
.params
.get(index)
.ok_or_else(|| MissingParameter(Box::new(self.to_owned()), index))?)
}
fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError> {
let mut message_text = self.try_get_param(1)?;
let is_action =
message_text.starts_with("\u{0001}ACTION ") && message_text.ends_with('\u{0001}');
if is_action {
message_text = &message_text[8..message_text.len() - 1];
}
Ok((message_text, is_action))
}
fn try_get_tag_value(&self, key: &'static str) -> Result<&str, ServerMessageParseError> {
match self.tags.0.get(key) {
Some(value) => Ok(value),
None => Err(MissingTag(Box::new(self.to_owned()), key)),
}
}
fn try_get_nonempty_tag_value(
&self,
key: &'static str,
) -> Result<&str, ServerMessageParseError> {
match self.tags.0.get(key) {
Some(value) => match value.as_str() {
"" => Err(MissingTagValue(Box::new(self.to_owned()), key)),
otherwise => Ok(otherwise),
},
None => Err(MissingTag(Box::new(self.to_owned()), key)),
}
}
fn try_get_optional_nonempty_tag_value(
&self,
key: &'static str,
) -> Result<Option<&str>, ServerMessageParseError> {
match self.tags.0.get(key) {
Some(value) => match value.as_str() {
"" => Err(MissingTagValue(Box::new(self.to_owned()), key)),
otherwise => Ok(Some(otherwise)),
},
None => Ok(None),
}
}
fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError> {
let param = self.try_get_param(0)?;
if !param.starts_with('#') || param.len() < 2 {
return Err(MalformedChannel(Box::new(self.to_owned())));
}
Ok(¶m[1..])
}
fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError> {
let param = self.try_get_param(0)?;
if param == "*" {
return Ok(None);
}
if !param.starts_with('#') || param.len() < 2 {
return Err(MalformedChannel(Box::new(self.to_owned())));
}
Ok(Some(¶m[1..]))
}
fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError> {
match &self.prefix {
None => Err(MissingPrefix(Box::new(self.to_owned()))),
Some(IRCPrefix::HostOnly { host: _ }) => {
Err(MissingNickname(Box::new(self.to_owned())))
}
Some(IRCPrefix::Full {
nick,
user: _,
host: _,
}) => Ok(nick),
}
}
fn try_get_emotes(
&self,
tag_key: &'static str,
message_text: &str,
) -> Result<Vec<Emote>, ServerMessageParseError> {
let tag_value = self.try_get_tag_value(tag_key)?;
if tag_value.is_empty() {
return Ok(vec![]);
}
let mut emotes = Vec::new();
let make_error =
|| MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
for src in tag_value.split('/') {
let (emote_id, indices_src) = src.split_once(':').ok_or_else(make_error)?;
for range_src in indices_src.split(',') {
let (start, end) = range_src.split_once('-').ok_or_else(make_error)?;
let start = usize::from_str(start).map_err(|_| make_error())?;
let end = usize::from_str(end).map_err(|_| make_error())? + 1;
let code_length = end - start;
let code = message_text
.chars()
.skip(start)
.take(code_length)
.collect::<String>();
emotes.push(Emote {
id: emote_id.to_owned(),
char_range: Range { start, end },
code,
});
}
}
emotes.sort_unstable_by_key(|e| e.char_range.start);
Ok(emotes)
}
fn try_get_emote_sets(
&self,
tag_key: &'static str,
) -> Result<HashSet<String>, ServerMessageParseError> {
let src = self.try_get_tag_value(tag_key)?;
if src.is_empty() {
Ok(HashSet::new())
} else {
Ok(src.split(',').map(|s| s.to_owned()).collect())
}
}
fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError> {
let tag_value = self.try_get_tag_value(tag_key)?;
if tag_value.is_empty() {
return Ok(vec![]);
}
let mut badges = Vec::new();
let make_error =
|| MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
for src in tag_value.split(',') {
let (name, version) = src.split_once('/').ok_or_else(make_error)?;
badges.push(Badge {
name: name.to_owned(),
version: version.to_owned(),
});
}
Ok(badges)
}
fn try_get_color(
&self,
tag_key: &'static str,
) -> Result<Option<RGBColor>, ServerMessageParseError> {
let tag_value = self.try_get_tag_value(tag_key)?;
let make_error =
|| MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
if tag_value.is_empty() {
return Ok(None);
}
if tag_value.len() != 7 {
return Err(make_error());
}
Ok(Some(RGBColor {
r: u8::from_str_radix(&tag_value[1..3], 16).map_err(|_| make_error())?,
g: u8::from_str_radix(&tag_value[3..5], 16).map_err(|_| make_error())?,
b: u8::from_str_radix(&tag_value[5..7], 16).map_err(|_| make_error())?,
}))
}
fn try_get_number<N: FromStr>(
&self,
tag_key: &'static str,
) -> Result<N, ServerMessageParseError> {
let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
let number = N::from_str(tag_value).map_err(|_| {
MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
})?;
Ok(number)
}
fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError> {
Ok(self.try_get_number::<u8>(tag_key)? > 0)
}
fn try_get_optional_number<N: FromStr>(
&self,
tag_key: &'static str,
) -> Result<Option<N>, ServerMessageParseError> {
let tag_value = match self.tags.0.get(tag_key) {
Some(value) => match value.as_str() {
"" => return Err(MissingTagValue(Box::new(self.to_owned()), tag_key)),
otherwise => otherwise,
},
None => return Ok(None),
};
let number = N::from_str(tag_value).map_err(|_| {
MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
})?;
Ok(Some(number))
}
fn try_get_optional_bool(
&self,
tag_key: &'static str,
) -> Result<Option<bool>, ServerMessageParseError> {
Ok(self.try_get_optional_number::<u8>(tag_key)?.map(|n| n > 0))
}
fn try_get_timestamp(
&self,
tag_key: &'static str,
) -> Result<DateTime<Utc>, ServerMessageParseError> {
let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
let milliseconds_since_epoch = i64::from_str(tag_value).map_err(|_| {
MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
})?;
Utc.timestamp_millis_opt(milliseconds_since_epoch)
.single()
.ok_or_else(|| {
MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
})
}
fn try_get_optional_reply_parent(
&self,
) -> Result<Option<ReplyParent>, ServerMessageParseError> {
if !self.tags.0.contains_key("reply-parent-msg-id") {
return Ok(None);
}
Ok(Some(ReplyParent {
message_id: self.try_get_tag_value("reply-parent-msg-id")?.to_owned(),
reply_parent_user: TwitchUserBasics {
id: self
.try_get_nonempty_tag_value("reply-parent-user-id")?
.to_owned(),
login: self
.try_get_nonempty_tag_value("reply-parent-user-login")?
.to_owned(),
name: self
.try_get_nonempty_tag_value("reply-parent-display-name")?
.to_owned(),
},
message_text: self.try_get_tag_value("reply-parent-msg-body")?.to_owned(),
}))
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
#[doc(hidden)]
pub struct HiddenIRCMessage(pub(self) IRCMessage);
#[derive(Debug, Clone)]
#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum ServerMessage {
ClearChat(ClearChatMessage),
ClearMsg(ClearMsgMessage),
GlobalUserState(GlobalUserStateMessage),
Join(JoinMessage),
Notice(NoticeMessage),
Part(PartMessage),
Ping(PingMessage),
Pong(PongMessage),
Privmsg(PrivmsgMessage),
Reconnect(ReconnectMessage),
RoomState(RoomStateMessage),
UserNotice(UserNoticeMessage),
UserState(UserStateMessage),
Whisper(WhisperMessage),
#[doc(hidden)]
Generic(HiddenIRCMessage),
}
impl TryFrom<IRCMessage> for ServerMessage {
type Error = ServerMessageParseError;
fn try_from(source: IRCMessage) -> Result<ServerMessage, ServerMessageParseError> {
use ServerMessage::{
ClearChat, ClearMsg, Generic, GlobalUserState, Join, Notice, Part, Ping, Pong, Privmsg,
Reconnect, RoomState, UserNotice, UserState, Whisper,
};
Ok(match source.command.as_str() {
"CLEARCHAT" => ClearChat(ClearChatMessage::try_from(source)?),
"CLEARMSG" => ClearMsg(ClearMsgMessage::try_from(source)?),
"GLOBALUSERSTATE" => GlobalUserState(GlobalUserStateMessage::try_from(source)?),
"JOIN" => Join(JoinMessage::try_from(source)?),
"NOTICE" => Notice(NoticeMessage::try_from(source)?),
"PART" => Part(PartMessage::try_from(source)?),
"PING" => Ping(PingMessage::try_from(source)?),
"PONG" => Pong(PongMessage::try_from(source)?),
"PRIVMSG" => Privmsg(PrivmsgMessage::try_from(source)?),
"RECONNECT" => Reconnect(ReconnectMessage::try_from(source)?),
"ROOMSTATE" => RoomState(RoomStateMessage::try_from(source)?),
"USERNOTICE" => UserNotice(UserNoticeMessage::try_from(source)?),
"USERSTATE" => UserState(UserStateMessage::try_from(source)?),
"WHISPER" => Whisper(WhisperMessage::try_from(source)?),
_ => Generic(HiddenIRCMessage(source)),
})
}
}
impl From<ServerMessage> for IRCMessage {
fn from(msg: ServerMessage) -> IRCMessage {
match msg {
ServerMessage::ClearChat(msg) => msg.source,
ServerMessage::ClearMsg(msg) => msg.source,
ServerMessage::GlobalUserState(msg) => msg.source,
ServerMessage::Join(msg) => msg.source,
ServerMessage::Notice(msg) => msg.source,
ServerMessage::Part(msg) => msg.source,
ServerMessage::Ping(msg) => msg.source,
ServerMessage::Pong(msg) => msg.source,
ServerMessage::Privmsg(msg) => msg.source,
ServerMessage::Reconnect(msg) => msg.source,
ServerMessage::RoomState(msg) => msg.source,
ServerMessage::UserNotice(msg) => msg.source,
ServerMessage::UserState(msg) => msg.source,
ServerMessage::Whisper(msg) => msg.source,
ServerMessage::Generic(msg) => msg.0,
}
}
}
impl ServerMessage {
#[must_use]
pub fn source(&self) -> &IRCMessage {
match self {
ServerMessage::ClearChat(msg) => &msg.source,
ServerMessage::ClearMsg(msg) => &msg.source,
ServerMessage::GlobalUserState(msg) => &msg.source,
ServerMessage::Join(msg) => &msg.source,
ServerMessage::Notice(msg) => &msg.source,
ServerMessage::Part(msg) => &msg.source,
ServerMessage::Ping(msg) => &msg.source,
ServerMessage::Pong(msg) => &msg.source,
ServerMessage::Privmsg(msg) => &msg.source,
ServerMessage::Reconnect(msg) => &msg.source,
ServerMessage::RoomState(msg) => &msg.source,
ServerMessage::UserNotice(msg) => &msg.source,
ServerMessage::UserState(msg) => &msg.source,
ServerMessage::Whisper(msg) => &msg.source,
ServerMessage::Generic(msg) => &msg.0,
}
}
pub(crate) fn new_generic(message: IRCMessage) -> ServerMessage {
ServerMessage::Generic(HiddenIRCMessage(message))
}
}
impl AsRawIRC for ServerMessage {
fn format_as_raw_irc(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.source().format_as_raw_irc(f)
}
}