Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for super reactions #2882

Merged
merged 3 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,12 +815,12 @@ impl Http {
.await
}

/// Reacts to a message.
pub async fn create_reaction(
async fn _create_reaction(
&self,
channel_id: ChannelId,
message_id: MessageId,
reaction_type: &ReactionType,
burst: bool,
) -> Result<()> {
self.wind(204, Request {
body: None,
Expand All @@ -832,11 +832,31 @@ impl Http {
message_id,
reaction: &reaction_type.as_data(),
},
params: None,
params: Some(vec![("burst", burst.to_string())]),
})
.await
}

/// Reacts to a message.
pub async fn create_reaction(
&self,
channel_id: ChannelId,
message_id: MessageId,
reaction_type: &ReactionType,
) -> Result<()> {
self._create_reaction(channel_id, message_id, reaction_type, false).await
}

/// Super reacts to a message.
pub async fn create_super_reaction(
&self,
channel_id: ChannelId,
message_id: MessageId,
reaction_type: &ReactionType,
) -> Result<()> {
self._create_reaction(channel_id, message_id, reaction_type, true).await
}

/// Creates a role.
pub async fn create_role(
&self,
Expand Down
69 changes: 65 additions & 4 deletions src/model/channel/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,13 +539,35 @@ impl Message {
cache_http: impl CacheHttp,
reaction_type: impl Into<ReactionType>,
) -> Result<Reaction> {
self._react(cache_http, reaction_type.into()).await
self._react(cache_http, reaction_type.into(), false).await
}

/// React to the message with a custom [`Emoji`] or unicode character.
///
/// **Note**: Requires [Add Reactions] and [Use External Emojis] permissions.
///
/// # Errors
///
/// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user
/// does not have the required [permissions].
///
/// [Add Reactions]: Permissions::ADD_REACTIONS
/// [Use External Emojis]: Permissions::USE_EXTERNAL_EMOJIS
/// [permissions]: crate::model::permissions
#[inline]
pub async fn super_react(
&self,
cache_http: impl CacheHttp,
reaction_type: impl Into<ReactionType>,
) -> Result<Reaction> {
self._react(cache_http, reaction_type.into(), true).await
}

async fn _react(
&self,
cache_http: impl CacheHttp,
reaction_type: ReactionType,
burst: bool,
) -> Result<Reaction> {
#[cfg_attr(not(feature = "cache"), allow(unused_mut))]
let mut user_id = None;
Expand All @@ -559,13 +581,30 @@ impl Message {
self.channel_id,
Permissions::ADD_REACTIONS,
)?;

if burst {
utils::user_has_perms_cache(
cache,
self.channel_id,
Permissions::USE_EXTERNAL_EMOJIS,
)?;
}
}

user_id = Some(cache.current_user().id);
}
}

cache_http.http().create_reaction(self.channel_id, self.id, &reaction_type).await?;
let reaction_types = if burst {
cache_http
.http()
.create_super_reaction(self.channel_id, self.id, &reaction_type)
.await?;
ReactionTypes::Burst
} else {
cache_http.http().create_reaction(self.channel_id, self.id, &reaction_type).await?;
ReactionTypes::Normal
};

Ok(Reaction {
channel_id: self.channel_id,
Expand All @@ -574,6 +613,10 @@ impl Message {
user_id,
guild_id: self.guild_id,
member: self.member.as_deref().map(|member| member.clone().into()),
message_author_id: None,
burst,
burst_colours: None,
reaction_type: reaction_types,
})
}

Expand Down Expand Up @@ -891,13 +934,31 @@ impl<'a> From<&'a Message> for MessageId {
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct MessageReaction {
/// The amount of the type of reaction that have been sent for the associated message.
/// The amount of the type of reaction that have been sent for the associated message
/// including super reactions.
pub count: u64,
/// Indicator of whether the current user has sent the type of reaction.
/// A breakdown of what reactions were from regular reactions and super reactions.
pub count_details: CountDetails,
/// Indicator of whether the current user has sent this type of reaction.
pub me: bool,
/// Indicator of whether the current user has sent the type of super-reaction.
pub me_burst: bool,
/// The type of reaction.
#[serde(rename = "emoji")]
pub reaction_type: ReactionType,
// The colours used for super reactions.
pub burst_colours: Vec<Colour>,
}

/// A representation of reaction count details.
///
/// [Discord docs](https://discord.com/developers/docs/resources/channel#reaction-count-details-object).
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct CountDetails {
pub burst: u64,
pub normal: u64,
}

enum_number! {
Expand Down
64 changes: 64 additions & 0 deletions src/model/channel/reaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::str::FromStr;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::de::Error as DeError;
use serde::ser::{Serialize, SerializeMap, Serializer};
use serde_cow::CowStr;
#[cfg(feature = "model")]
use tracing::warn;

Expand Down Expand Up @@ -36,9 +37,38 @@ pub struct Reaction {
/// The optional Id of the [`Guild`] where the reaction was sent.
pub guild_id: Option<GuildId>,
/// The optional object of the member which added the reaction.
///
/// Not present on the ReactionRemove gateway event.
pub member: Option<Member>,
/// The reactive emoji used.
pub emoji: ReactionType,
/// The Id of the user who sent the message which this reacted to.
///
/// Only present on the ReactionAdd gateway event.
pub message_author_id: Option<UserId>,
/// Indicates if this was a super reaction.
pub burst: bool,
/// Colours used for the super reaction animation.
///
/// Only present on the ReactionAdd gateway event.
#[serde(rename = "burst_colors", default, deserialize_with = "discord_colours")]
pub burst_colours: Option<Vec<Colour>>,
/// The type of reaction.
#[serde(rename = "type")]
pub reaction_type: ReactionTypes,
}

enum_number! {
/// A list of types a reaction can be.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum ReactionTypes {
Normal = 0,
Burst = 1,
_ => Unknown(u8),
}
}

// Manual impl needed to insert guild_id into PartialMember
Expand All @@ -58,6 +88,40 @@ impl Serialize for Reaction {
}
}

fn discord_colours<'de, D>(deserializer: D) -> Result<Option<Vec<Colour>>, D::Error>
where
D: Deserializer<'de>,
{
let vec_str: Option<Vec<CowStr<'_>>> = Deserialize::deserialize(deserializer)?;

let Some(vec_str) = vec_str else { return Ok(None) };

if vec_str.is_empty() {
return Ok(None);
}

let colours: Result<Vec<_>, _> = vec_str
.iter()
.map(|s| {
let s = s.0.strip_prefix('#').ok_or_else(|| DeError::custom("Invalid colour data"))?;

if s.len() != 6 {
return Err(DeError::custom("Invalid colour data length"));
}

match u32::from_str_radix(s, 16) {
Ok(c) => Ok(Colour::new(c)),
Err(_) => Err(DeError::custom("Invalid colour data")),
}
})
.collect();

match colours {
Ok(colours) => Ok(Some(colours)),
Err(err) => Err(err),
}
}

#[cfg(feature = "model")]
impl Reaction {
/// Retrieves the associated the reaction was made in.
Expand Down
Loading