diff --git a/twilight-cache-inmemory/src/event/reaction.rs b/twilight-cache-inmemory/src/event/reaction.rs index 7ef5894257b..d18983e9bb6 100644 --- a/twilight-cache-inmemory/src/event/reaction.rs +++ b/twilight-cache-inmemory/src/event/reaction.rs @@ -1,6 +1,6 @@ use crate::{config::ResourceType, InMemoryCache, UpdateCache}; use twilight_model::{ - channel::message::{Reaction, ReactionType}, + channel::message::{Reaction, ReactionCountDetails, ReactionType}, gateway::payload::incoming::{ ReactionAdd, ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji, }, @@ -39,9 +39,15 @@ impl UpdateCache for ReactionAdd { .unwrap_or_default(); message.reactions.push(Reaction { + burst_colors: Vec::new(), count: 1, + count_details: ReactionCountDetails { + burst: 0, + normal: 1, + }, emoji: self.0.emoji.clone(), me, + me_burst: false, }); } } diff --git a/twilight-model/src/channel/message/mod.rs b/twilight-model/src/channel/message/mod.rs index 6dba6c160d8..cf262e16be6 100644 --- a/twilight-model/src/channel/message/mod.rs +++ b/twilight-model/src/channel/message/mod.rs @@ -26,7 +26,7 @@ pub use self::{ interaction::MessageInteraction, kind::MessageType, mention::Mention, - reaction::{Reaction, ReactionType}, + reaction::{Reaction, ReactionCountDetails, ReactionType}, reference::MessageReference, role_subscription_data::RoleSubscriptionData, sticker::Sticker, @@ -196,6 +196,7 @@ pub struct Message { #[cfg(test)] mod tests { use super::{ + reaction::ReactionCountDetails, sticker::{MessageSticker, StickerFormatType}, Message, MessageActivity, MessageActivityType, MessageApplication, MessageFlags, MessageReference, MessageType, Reaction, ReactionType, @@ -478,11 +479,17 @@ mod tests { mentions: Vec::new(), pinned: false, reactions: vec![Reaction { + burst_colors: Vec::new(), count: 7, + count_details: ReactionCountDetails { + burst: 0, + normal: 7, + }, emoji: ReactionType::Unicode { name: "a".to_owned(), }, me: true, + me_burst: false, }], reference: Some(MessageReference { channel_id: Some(Id::new(1)), @@ -653,10 +660,23 @@ mod tests { Token::Seq { len: Some(1) }, Token::Struct { name: "Reaction", - len: 3, + len: 6, }, + Token::Str("burst_colors"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, Token::Str("count"), Token::U64(7), + Token::Str("count_details"), + Token::Struct { + name: "ReactionCountDetails", + len: 2, + }, + Token::Str("burst"), + Token::U64(0), + Token::Str("normal"), + Token::U64(7), + Token::StructEnd, Token::Str("emoji"), Token::Struct { name: "ReactionType", @@ -667,6 +687,8 @@ mod tests { Token::StructEnd, Token::Str("me"), Token::Bool(true), + Token::Str("me_burst"), + Token::Bool(false), Token::StructEnd, Token::SeqEnd, Token::Str("message_reference"), diff --git a/twilight-model/src/channel/message/reaction.rs b/twilight-model/src/channel/message/reaction.rs index e1ddee51af8..6538529554a 100644 --- a/twilight-model/src/channel/message/reaction.rs +++ b/twilight-model/src/channel/message/reaction.rs @@ -1,15 +1,24 @@ -use crate::id::{marker::EmojiMarker, Id}; +use crate::{ + id::{marker::EmojiMarker, Id}, + util::HexColor, +}; use serde::{Deserialize, Serialize}; /// Reaction below a message. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Reaction { + /// HEX colors used for super reaction. + pub burst_colors: Vec, /// Amount of reactions this emoji has. pub count: u64, + /// Reaction count details for each type of reaction. + pub count_details: ReactionCountDetails, /// Emoji of this reaction. pub emoji: ReactionType, /// Whether the current user has reacted with this emoji. pub me: bool, + /// Whether the current user super-reacted using this emoji + pub me_burst: bool, } /// Type of [`Reaction`]. @@ -46,20 +55,35 @@ pub enum ReactionType { }, } +/// Breakdown of normal and super reaction counts for the associated emoji. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +pub struct ReactionCountDetails { + /// Count of super reactions. + pub burst: u64, + /// Count of normal reactions. + pub normal: u64, +} + #[cfg(test)] mod tests { - use super::{Reaction, ReactionType}; - use crate::id::Id; + use super::{Reaction, ReactionCountDetails, ReactionType}; + use crate::{id::Id, util::HexColor}; use serde_test::Token; #[test] fn message_reaction_unicode() { let value = Reaction { + burst_colors: Vec::from([HexColor(255, 255, 255)]), count: 7, + count_details: ReactionCountDetails { + burst: 0, + normal: 7, + }, emoji: ReactionType::Unicode { name: "a".to_owned(), }, me: true, + me_burst: false, }; serde_test::assert_tokens( @@ -67,10 +91,24 @@ mod tests { &[ Token::Struct { name: "Reaction", - len: 3, + len: 6, }, + Token::Str("burst_colors"), + Token::Seq { len: Some(1) }, + Token::Str("#FFFFFF"), + Token::SeqEnd, Token::Str("count"), Token::U64(7), + Token::Str("count_details"), + Token::Struct { + name: "ReactionCountDetails", + len: 2, + }, + Token::Str("burst"), + Token::U64(0), + Token::Str("normal"), + Token::U64(7), + Token::StructEnd, Token::Str("emoji"), Token::Struct { name: "ReactionType", @@ -81,6 +119,8 @@ mod tests { Token::StructEnd, Token::Str("me"), Token::Bool(true), + Token::Str("me_burst"), + Token::Bool(false), Token::StructEnd, ], ); diff --git a/twilight-model/src/util/hex_color.rs b/twilight-model/src/util/hex_color.rs new file mode 100644 index 00000000000..fb66b0ed6b9 --- /dev/null +++ b/twilight-model/src/util/hex_color.rs @@ -0,0 +1,130 @@ +use std::fmt::Formatter; +use std::fmt::{Display, Result as FmtResult}; +use std::num::ParseIntError; +use std::str::FromStr; + +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Represents a color in the RGB format using hexadecimal notation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct HexColor( + /// Red component of the color. + pub u8, + /// Green component of the color. + pub u8, + /// Blue component of the color. + pub u8, +); + +impl Display for HexColor { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.write_fmt(format_args!("#{:02X}{:02X}{:02X}", self.0, self.1, self.2)) + } +} + +impl Serialize for HexColor { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +pub enum HexColorParseError { + InvalidLength, + InvalidFormat, + InvalidCharacter(ParseIntError), +} + +impl From for HexColorParseError { + fn from(err: ParseIntError) -> Self { + Self::InvalidCharacter(err) + } +} + +impl FromStr for HexColor { + type Err = HexColorParseError; + + fn from_str(s: &str) -> Result { + if !s.starts_with('#') { + return Err(HexColorParseError::InvalidFormat); + } + + let s = s.trim_start_matches('#'); + + let (r, g, b) = match s.len() { + 3 => ( + u8::from_str_radix(&s[0..1], 16)?, + u8::from_str_radix(&s[1..2], 16)?, + u8::from_str_radix(&s[2..3], 16)?, + ), + 6 => ( + u8::from_str_radix(&s[0..2], 16)?, + u8::from_str_radix(&s[2..4], 16)?, + u8::from_str_radix(&s[4..6], 16)?, + ), + _ => return Err(HexColorParseError::InvalidLength), + }; + + Ok(Self(r, g, b)) + } +} + +struct HexColorVisitor; + +impl<'de> Visitor<'de> for HexColorVisitor { + type Value = HexColor; + + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + formatter.write_str("a hex color string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + HexColor::from_str(v).map_err(|_| E::custom("invalid hex color")) + } +} + +impl<'de> Deserialize<'de> for HexColor { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(HexColorVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::HexColor; + + #[test] + fn hex_color_display() { + let hex_color = HexColor(255, 255, 255); + assert_eq!(hex_color.to_string(), "#FFFFFF"); + } + + #[test] + fn serialize() { + let hex_color = HexColor(252, 177, 3); + let serialized = serde_json::to_string(&hex_color).unwrap(); + assert_eq!(serialized, "\"#FCB103\""); + } + + #[test] + fn serialize_2() { + let hex_color = HexColor(255, 255, 255); + let serialized = serde_json::to_string(&hex_color).unwrap(); + assert_eq!(serialized, "\"#FFFFFF\""); + } + + #[test] + fn deserialize() { + let deserialized: HexColor = serde_json::from_str("\"#FFFFFF\"").unwrap(); + assert_eq!(deserialized, HexColor(255, 255, 255)); + } + + #[test] + fn deserialize_invalid() { + let deserialized: Result = serde_json::from_str("\"#GGGGGG\""); + assert!(deserialized.is_err()); + } +} diff --git a/twilight-model/src/util/mod.rs b/twilight-model/src/util/mod.rs index b1d0a6e9f5f..c30df90e120 100644 --- a/twilight-model/src/util/mod.rs +++ b/twilight-model/src/util/mod.rs @@ -1,9 +1,10 @@ //! Utilities for efficiently parsing and representing data from Discord's API. pub mod datetime; +pub mod hex_color; pub mod image_hash; -pub use self::{datetime::Timestamp, image_hash::ImageHash}; +pub use self::{datetime::Timestamp, hex_color::HexColor, image_hash::ImageHash}; #[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) fn is_false(value: &bool) -> bool {