use crate::{irc::*, MaybeOwned, MaybeOwnedIndex, Validator};
use crate::twitch::{
parse_badges, parse_badges_iter, parse_emotes, Badge, BadgeInfo, BadgeKind, Color, Emotes,
};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub enum Ctcp<'a> {
Action,
Unknown {
command: &'a str,
},
}
#[derive(Clone, PartialEq)]
pub struct Privmsg<'a> {
raw: MaybeOwned<'a>,
tags: TagIndices,
name: MaybeOwnedIndex,
channel: MaybeOwnedIndex,
data: MaybeOwnedIndex,
ctcp: Option<MaybeOwnedIndex>,
}
#[derive(Debug)]
pub struct BadgesIter<'a> {
items: Option<std::str::Split<'a, char>>,
}
impl<'a> Iterator for BadgesIter<'a> {
type Item = Badge<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(item) = self.items.as_mut()?.next() {
Badge::parse(item)
} else {
None
}
}
}
#[derive(Debug)]
pub struct EmotesIter<'a> {
items: Option<std::str::SplitTerminator<'a, char>>,
}
impl<'a> Iterator for EmotesIter<'a> {
type Item = Emotes;
fn next(&mut self) -> Option<Self::Item> {
if let Some(item) = self.items.as_mut()?.next() {
Emotes::parse_item(item)
} else {
None
}
}
}
impl<'a> Privmsg<'a> {
raw!();
tags!();
str_field!(
name
);
str_field!(
channel
);
str_field!(
data
);
pub fn iter_badges(&self) -> BadgesIter {
BadgesIter {
items: self.tags().get("badges").map(|s| s.split(',')),
}
}
pub fn iter_emotes(&self) -> EmotesIter {
EmotesIter {
items: self.tags().get("emotes").map(|s| s.split_terminator('/')),
}
}
pub fn ctcp(&self) -> Option<Ctcp<'_>> {
const ACTION: &str = "ACTION";
let command = &self.raw[self.ctcp?];
if command == ACTION {
Some(Ctcp::Action)
} else {
Some(Ctcp::Unknown { command })
}
}
pub fn is_action(&self) -> bool {
matches!(self.ctcp(), Some(Ctcp::Action))
}
pub fn badge_info(&'a self) -> Vec<BadgeInfo<'a>> {
self.tags()
.get("badge-info")
.map(parse_badges)
.unwrap_or_default()
}
pub fn badges(&'a self) -> Vec<Badge<'a>> {
self.tags()
.get("badges")
.map(parse_badges)
.unwrap_or_default()
}
pub fn bits(&self) -> Option<u64> {
self.tags().get_parsed("bits")
}
pub fn color(&self) -> Option<Color> {
self.tags().get_parsed("color")
}
pub fn display_name(&'a self) -> Option<&str> {
self.tags().get("display-name")
}
pub fn emotes(&self) -> Vec<Emotes> {
self.tags()
.get("emotes")
.map(parse_emotes)
.unwrap_or_default()
}
pub fn is_broadcaster(&self) -> bool {
self.contains_badge(BadgeKind::Broadcaster)
}
pub fn is_moderator(&self) -> bool {
self.contains_badge(BadgeKind::Moderator)
}
pub fn is_vip(&self) -> bool {
self.contains_badge(BadgeKind::Broadcaster)
}
pub fn is_subscriber(&self) -> bool {
self.contains_badge(BadgeKind::Subscriber)
}
pub fn is_staff(&self) -> bool {
self.contains_badge(BadgeKind::Staff)
}
pub fn is_turbo(&self) -> bool {
self.contains_badge(BadgeKind::Turbo)
}
pub fn is_global_moderator(&self) -> bool {
self.contains_badge(BadgeKind::GlobalMod)
}
pub fn room_id(&self) -> Option<u64> {
self.tags().get_parsed("room-id")
}
pub fn tmi_sent_ts(&self) -> Option<u64> {
self.tags().get_parsed("tmi-sent-ts")
}
pub fn user_id(&self) -> Option<u64> {
self.tags().get_parsed("user-id")
}
pub fn custom_reward_id(&self) -> Option<&str> {
self.tags().get("custom-reward-id")
}
pub fn msg_id(&self) -> Option<&str> {
self.tags().get("msg-id")
}
fn contains_badge(&self, badge: BadgeKind<'_>) -> bool {
self.tags()
.get("badges")
.into_iter()
.flat_map(parse_badges_iter)
.any(|x| x.kind == badge)
}
}
impl<'a> FromIrcMessage<'a> for Privmsg<'a> {
type Error = MessageError;
fn from_irc(msg: IrcMessage<'a>) -> Result<Self, Self::Error> {
const CTCP_MARKER: char = '\x01';
msg.expect_command(IrcMessage::PRIVMSG)?;
let mut index = msg.expect_data_index()?;
let mut ctcp = None;
let data = &msg.raw[index];
if data.starts_with(CTCP_MARKER) && data.ends_with(CTCP_MARKER) {
let len = data.chars().map(char::len_utf8).sum::<usize>();
match data[1..len - 1].find(' ') {
Some(pos) => {
let head = index.start + 1;
let ctcp_index = MaybeOwnedIndex::raw(head as usize, (head as usize) + pos);
index.start += (pos as u16) + 2;
index.end -= 1;
ctcp.replace(ctcp_index);
}
None => return Err(MessageError::ExpectedData),
}
}
let this = Self {
tags: msg.parse_tags(),
name: msg.expect_nick()?,
channel: msg.expect_arg_index(0)?,
data: index,
ctcp,
raw: msg.raw,
};
Ok(this)
}
into_inner_raw!();
}
into_owned!(Privmsg {
raw,
tags,
name,
channel,
data,
ctcp,
});
impl_custom_debug!(Privmsg {
raw,
tags,
name,
channel,
data,
ctcp,
});
serde_struct!(Privmsg {
raw,
tags,
name,
channel,
data,
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "serde")]
fn privmsg_serde() {
let input = &[
":test!user@host PRIVMSG #museun :this is a test\r\n",
":test!user@host PRIVMSG #museun :\u{FFFD}\u{1F468}\r\n",
":test!user@host PRIVMSG #museun :\x01ACTION this is a test\x01\r\n",
":test!user@host PRIVMSG #museun :\x01FOOBAR this is a test\x01\r\n",
];
for input in input {
crate::serde::round_trip_json::<Privmsg>(input);
crate::serde::round_trip_rmp::<Privmsg>(input);
}
}
#[test]
fn privmsg() {
let input = ":test!user@host PRIVMSG #museun :this is a test\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.name(), "test");
assert_eq!(msg.channel(), "#museun");
assert_eq!(msg.data(), "this is a test");
assert_eq!(msg.ctcp(), None);
}
}
#[test]
fn privmsg_boundary() {
let input = ":test!user@host PRIVMSG #museun :\u{FFFD}\u{1F468}\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.name(), "test");
assert_eq!(msg.channel(), "#museun");
assert_eq!(msg.data(), "\u{FFFD}\u{1F468}");
assert_eq!(msg.ctcp(), None);
}
}
#[test]
fn privmsg_action() {
let input = ":test!user@host PRIVMSG #museun :\x01ACTION this is a test\x01\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.name(), "test");
assert_eq!(msg.channel(), "#museun");
assert_eq!(msg.data(), "this is a test");
assert_eq!(msg.ctcp().unwrap(), Ctcp::Action);
}
}
#[test]
fn privmsg_unknown() {
let input = ":test!user@host PRIVMSG #museun :\x01FOOBAR this is a test\x01\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.name(), "test");
assert_eq!(msg.channel(), "#museun");
assert_eq!(msg.data(), "this is a test");
assert_eq!(msg.ctcp().unwrap(), Ctcp::Unknown { command: "FOOBAR" });
}
}
#[test]
fn privmsg_community_rewards() {
let input = "@custom-reward-id=abc-123-foo;msg-id=highlighted-message :test!user@host PRIVMSG #museun :Notice me!\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.name(), "test");
assert_eq!(msg.channel(), "#museun");
assert_eq!(msg.data(), "Notice me!");
assert_eq!(msg.custom_reward_id().unwrap(), "abc-123-foo");
assert_eq!(msg.msg_id().unwrap(), "highlighted-message");
}
}
#[test]
fn privmsg_badges_iter() {
let input = "@badge-info=;badges=broadcaster/1;color=#FF69B4;display-name=museun;emote-only=1;emotes=25:0-4,6-10/81274:12-17;flags=;id=4e160a53-5482-4764-ba28-f224cd59a51f;mod=0;room-id=23196011;subscriber=0;tmi-sent-ts=1601079032426;turbo=0;user-id=23196011;user-type= :museun!museun@museun.tmi.twitch.tv PRIVMSG #museun :Kappa Kappa VoHiYo\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.iter_badges().count(), 1);
}
}
#[test]
fn privmsg_emotes_iter() {
let input = "@badge-info=;badges=broadcaster/1;color=#FF69B4;display-name=museun;emote-only=1;emotes=25:0-4,6-10/81274:12-17;flags=;id=4e160a53-5482-4764-ba28-f224cd59a51f;mod=0;room-id=23196011;subscriber=0;tmi-sent-ts=1601079032426;turbo=0;user-id=23196011;user-type= :museun!museun@museun.tmi.twitch.tv PRIVMSG #museun :Kappa Kappa VoHiYo\r\n";
for msg in parse(input).map(|s| s.unwrap()) {
let msg = Privmsg::from_irc(msg).unwrap();
assert_eq!(msg.iter_emotes().count(), 2);
}
}
}