Skip to content

Commit

Permalink
feat(model, cache): add support for super reactions (#2275)
Browse files Browse the repository at this point in the history
Ref:
- discord/discord-api-docs#6056

This PR also adds a new `HexColor` struct for efficiently storing hex
strings.
  • Loading branch information
suneettipirneni authored Oct 7, 2023
1 parent 00600fa commit c269fe5
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 8 deletions.
8 changes: 7 additions & 1 deletion twilight-cache-inmemory/src/event/reaction.rs
Original file line number Diff line number Diff line change
@@ -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,
},
Expand Down Expand Up @@ -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,
});
}
}
Expand Down
26 changes: 24 additions & 2 deletions twilight-model/src/channel/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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",
Expand All @@ -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"),
Expand Down
48 changes: 44 additions & 4 deletions twilight-model/src/channel/message/reaction.rs
Original file line number Diff line number Diff line change
@@ -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<HexColor>,
/// 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`].
Expand Down Expand Up @@ -46,31 +55,60 @@ 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(
&value,
&[
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",
Expand All @@ -81,6 +119,8 @@ mod tests {
Token::StructEnd,
Token::Str("me"),
Token::Bool(true),
Token::Str("me_burst"),
Token::Bool(false),
Token::StructEnd,
],
);
Expand Down
130 changes: 130 additions & 0 deletions twilight-model/src/util/hex_color.rs
Original file line number Diff line number Diff line change
@@ -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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}

pub enum HexColorParseError {
InvalidLength,
InvalidFormat,
InvalidCharacter(ParseIntError),
}

impl From<ParseIntError> for HexColorParseError {
fn from(err: ParseIntError) -> Self {
Self::InvalidCharacter(err)
}
}

impl FromStr for HexColor {
type Err = HexColorParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
HexColor::from_str(v).map_err(|_| E::custom("invalid hex color"))
}
}

impl<'de> Deserialize<'de> for HexColor {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<HexColor, _> = serde_json::from_str("\"#GGGGGG\"");
assert!(deserialized.is_err());
}
}
3 changes: 2 additions & 1 deletion twilight-model/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit c269fe5

Please sign in to comment.