use std::{
collections::HashMap,
fmt,
num::{NonZeroU16, NonZeroU32}
};
use irc::proto::{Command, Response};
use uuid::Uuid;
use crate::util::{MapNonempty, get_utf8_slice};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum UserRole {
Normal,
Broadcaster,
Moderator,
GlobalModerator,
TwitchAdmin,
TwitchStaff
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct User {
pub username: String,
pub display_name: String,
pub id: u64,
pub display_color: Option<u32>,
pub sub_months: Option<NonZeroU16>,
pub role: UserRole,
pub returning_chatter: bool
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "t"))]
pub enum MessageSegment {
Text {
text: String
},
Emote {
name: String,
id: String
}
}
impl fmt::Display for MessageSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Text { text } => f.write_str(text),
Self::Emote { name, .. } => f.write_str(name)
}
}
}
#[derive(Debug)]
pub enum ChatEvent {
Message {
id: Uuid,
user: User,
sent_at_ms: i64,
reply_to: Option<Uuid>,
emote_only: bool,
first_message: bool,
contents: Vec<MessageSegment>
},
SendBits {
id: Uuid,
user: User,
bits: NonZeroU32,
sent_at_ms: i64,
segments: Vec<MessageSegment>
},
MemberChunk {
names: Vec<String>
},
EndOfMembers
}
pub(crate) fn to_chat_event(message: irc::proto::Message) -> Option<ChatEvent> {
match message.command {
Command::PRIVMSG(_, msg) => {
let mut tags = message
.tags?
.into_iter()
.filter(|c| c.1.is_some())
.map(|c| (c.0, c.1.expect("infallible")))
.collect::<HashMap<_, _>>();
let (username, user_display_name) = match message.prefix? {
irc::proto::Prefix::Nickname(n1, n2, _) => (
n1,
match tags.remove("display-name") {
Some(display_name) => {
if display_name.is_empty() {
n2
} else {
display_name
}
}
None => n2
}
),
_ => return None
};
let mut badges = tags
.remove("badges")
.and_then_nonempty(|c| {
c.split(',')
.map(|f| {
let mut split = f.splitn(2, '/');
Some((split.next()?.to_owned(), split.next()?.to_owned()))
})
.collect::<Option<HashMap<_, _>>>()
})
.unwrap_or_default();
let mut badge_info = tags
.remove("badge-info")
.and_then_nonempty(|c| {
c.split(',')
.map(|f| {
let mut split = f.splitn(2, '/');
Some((split.next()?.to_owned(), split.next()?.to_owned()))
})
.collect::<Option<HashMap<_, _>>>()
})
.unwrap_or_default();
let color = tags.remove("color").and_then_nonempty(|c| u32::from_str_radix(&c[1..], 16).ok());
let mut emotes = vec![];
for emote in tags.remove("emotes")?.split('/') {
if emote.is_empty() {
break;
}
let mut split = emote.splitn(2, ':');
let (id, ranges) = (split.next()?, split.next()?);
for range in ranges.split(',') {
let mut split = range.splitn(2, '-');
let (from, to) = (split.next().and_then(|f| f.parse::<usize>().ok())?, split.next().and_then(|f| f.parse::<usize>().ok())?);
emotes.push((id.to_owned(), from, to));
}
}
emotes.sort_by_key(|a| a.1);
let mut segments = Vec::with_capacity(emotes.len());
if !emotes.is_empty() {
let mut i = 0;
for (id, start, end) in emotes {
if start > i {
segments.push(MessageSegment::Text {
text: get_utf8_slice(&msg, i, start)?.to_owned()
});
}
if end >= start {
segments.push(MessageSegment::Emote {
name: get_utf8_slice(&msg, start, end + 1)?.to_owned(),
id
});
i = end + 1;
}
}
if i < msg.len() {
segments.push(MessageSegment::Text {
text: get_utf8_slice(&msg, i, msg.len())?.to_string()
});
}
} else {
segments.push(MessageSegment::Text { text: msg });
}
let user = User {
username,
display_name: user_display_name,
display_color: color,
role: match tags.remove("user-type").as_deref() {
Some("admin") => UserRole::TwitchAdmin,
Some("global_mod") => UserRole::GlobalModerator,
Some("staff") => UserRole::TwitchStaff,
_ => match tags.remove("mod").as_deref() {
Some("1") => UserRole::Moderator,
_ => match badges.remove("broadcaster").as_deref() {
Some(_) => UserRole::Broadcaster,
_ => UserRole::Normal
}
}
},
returning_chatter: matches!(tags.remove("returning-chatter").as_deref(), Some("1")),
sub_months: badge_info.remove("subscriber").and_then(|f| f.parse().ok()),
id: tags.remove("user-id").and_then(|f| f.parse().ok())?
};
let id = tags.remove("id").and_then(|f| f.parse().ok())?;
let sent_at = tags.remove("tmi-sent-ts").and_then(|f| f.parse::<i64>().ok())?;
if let Some(bits) = tags.remove("bits").and_then_nonempty(|f| f.parse().ok()) {
return Some(ChatEvent::SendBits {
id,
user,
bits,
sent_at_ms: sent_at,
segments
});
}
Some(ChatEvent::Message {
id,
user,
reply_to: tags.remove("reply-parent-msg-id").and_then(|f| f.parse().ok()),
sent_at_ms: sent_at,
emote_only: matches!(tags.remove("emote-only").as_deref(), Some("1")),
first_message: matches!(tags.remove("first-msg").as_deref(), Some("1")),
contents: segments
})
}
Command::Response(Response::RPL_NAMREPLY, names) => Some(ChatEvent::MemberChunk { names: names[3..].to_vec() }),
Command::Response(Response::RPL_ENDOFNAMES, _) => Some(ChatEvent::EndOfMembers),
_ => None
}
}