serenity/model/channel/
reaction.rs

1use std::cmp::Ordering;
2#[cfg(doc)]
3use std::fmt::Display as _;
4use std::fmt::{self, Write as _};
5use std::str::FromStr;
6
7#[cfg(feature = "http")]
8use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
9use serde::de::Error as DeError;
10use serde::ser::{Serialize, SerializeMap, Serializer};
11#[cfg(feature = "model")]
12use tracing::warn;
13
14#[cfg(feature = "model")]
15use crate::http::{CacheHttp, Http};
16use crate::internal::prelude::*;
17use crate::model::prelude::*;
18use crate::model::utils::discord_colours_opt;
19
20/// An emoji reaction to a message.
21///
22/// [Discord docs](https://discord.com/developers/docs/topics/gateway#message-reaction-add-message-reaction-add-event-fields).
23#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
24#[derive(Clone, Debug, Deserialize, Serialize)]
25#[serde(remote = "Self")]
26#[non_exhaustive]
27pub struct Reaction {
28    /// The Id of the [`User`] that sent the reaction.
29    ///
30    /// Always present when received from gateway.
31    /// Set to [`None`] by [`Message::react`] when cache is not available.
32    pub user_id: Option<UserId>,
33    /// The [`Channel`] of the associated [`Message`].
34    pub channel_id: ChannelId,
35    /// The Id of the [`Message`] that was reacted to.
36    pub message_id: MessageId,
37    /// The optional Id of the [`Guild`] where the reaction was sent.
38    pub guild_id: Option<GuildId>,
39    /// The optional object of the member which added the reaction.
40    ///
41    /// Not present on the ReactionRemove gateway event.
42    pub member: Option<Member>,
43    /// The reactive emoji used.
44    pub emoji: ReactionType,
45    /// The Id of the user who sent the message which this reacted to.
46    ///
47    /// Only present on the ReactionAdd gateway event.
48    pub message_author_id: Option<UserId>,
49    /// Indicates if this was a super reaction.
50    pub burst: bool,
51    /// Colours used for the super reaction animation.
52    ///
53    /// Only present on the ReactionAdd gateway event.
54    #[serde(rename = "burst_colors", default, deserialize_with = "discord_colours_opt")]
55    pub burst_colours: Option<Vec<Colour>>,
56    /// The type of reaction.
57    #[serde(rename = "type")]
58    pub reaction_type: ReactionTypes,
59}
60
61enum_number! {
62    /// A list of types a reaction can be.
63    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
64    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
65    #[serde(from = "u8", into = "u8")]
66    #[non_exhaustive]
67    pub enum ReactionTypes {
68        Normal = 0,
69        Burst = 1,
70        _ => Unknown(u8),
71    }
72}
73
74// Manual impl needed to insert guild_id into PartialMember
75impl<'de> Deserialize<'de> for Reaction {
76    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
77        let mut reaction = Self::deserialize(deserializer)?; // calls #[serde(remote)]-generated inherent method
78        if let (Some(guild_id), Some(member)) = (reaction.guild_id, reaction.member.as_mut()) {
79            member.guild_id = guild_id;
80        }
81        Ok(reaction)
82    }
83}
84
85impl Serialize for Reaction {
86    fn serialize<S: serde::Serializer>(&self, serializer: S) -> StdResult<S::Ok, S::Error> {
87        Self::serialize(self, serializer) // calls #[serde(remote)]-generated inherent method
88    }
89}
90
91#[cfg(feature = "model")]
92impl Reaction {
93    /// Retrieves the associated the reaction was made in.
94    ///
95    /// If the cache is enabled, this will search for the already-cached channel. If not - or the
96    /// channel was not found - this will perform a request over the REST API for the channel.
97    ///
98    /// Requires the [Read Message History] permission.
99    ///
100    /// # Errors
101    ///
102    /// Returns [`Error::Http`] if the current user lacks permission, or if the channel no longer
103    /// exists.
104    ///
105    /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY
106    #[inline]
107    pub async fn channel(&self, cache_http: impl CacheHttp) -> Result<Channel> {
108        self.channel_id.to_channel(cache_http).await
109    }
110
111    /// Deletes the reaction, but only if the current user is the user who made the reaction or has
112    /// permission to.
113    ///
114    /// Requires the [Manage Messages] permission, _if_ the current user did not perform the
115    /// reaction.
116    ///
117    /// # Errors
118    ///
119    /// If the `cache` is enabled, then returns a [`ModelError::InvalidPermissions`] if the current
120    /// user does not have the required [permissions].
121    ///
122    /// Otherwise returns [`Error::Http`] if the current user lacks permission.
123    ///
124    /// [Manage Messages]: Permissions::MANAGE_MESSAGES
125    /// [permissions]: crate::model::permissions
126    pub async fn delete(&self, cache_http: impl CacheHttp) -> Result<()> {
127        #[cfg_attr(not(feature = "cache"), allow(unused_mut))]
128        let mut user_id = self.user_id;
129
130        #[cfg(feature = "cache")]
131        {
132            if let Some(cache) = cache_http.cache() {
133                if self.user_id == Some(cache.current_user().id) {
134                    user_id = None;
135                }
136
137                if user_id.is_some() {
138                    crate::utils::user_has_perms_cache(
139                        cache,
140                        self.channel_id,
141                        Permissions::MANAGE_MESSAGES,
142                    )?;
143                }
144            }
145        }
146
147        self.channel_id
148            .delete_reaction(cache_http.http(), self.message_id, user_id, self.emoji.clone())
149            .await
150    }
151
152    /// Deletes all reactions from the message with this emoji.
153    ///
154    /// Requires the [Manage Messages] permission
155    ///
156    /// # Errors
157    ///
158    /// If the `cache` is enabled, then returns a [`ModelError::InvalidPermissions`] if the current
159    /// user does not have the required [permissions].
160    ///
161    /// Otherwise returns [`Error::Http`] if the current user lacks permission.
162    ///
163    /// [Manage Messages]: Permissions::MANAGE_MESSAGES
164    /// [permissions]: crate::model::permissions
165    pub async fn delete_all(&self, cache_http: impl CacheHttp) -> Result<()> {
166        #[cfg(feature = "cache")]
167        {
168            if let Some(cache) = cache_http.cache() {
169                crate::utils::user_has_perms_cache(
170                    cache,
171                    self.channel_id,
172                    Permissions::MANAGE_MESSAGES,
173                )?;
174            }
175        }
176        cache_http
177            .http()
178            .as_ref()
179            .delete_message_reaction_emoji(self.channel_id, self.message_id, &self.emoji)
180            .await
181    }
182
183    /// Retrieves the [`Message`] associated with this reaction.
184    ///
185    /// Requires the [Read Message History] permission.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`Error::Http`] if the current user lacks permission to read message history, or if
190    /// the message was deleted.
191    ///
192    /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY
193    #[inline]
194    pub async fn message(&self, cache_http: impl CacheHttp) -> Result<Message> {
195        self.channel_id.message(cache_http, self.message_id).await
196    }
197
198    /// Retrieves the user that made the reaction.
199    ///
200    /// If the cache is enabled, this will search for the already-cached user. If not - or the user
201    /// was not found - this will perform a request over the REST API for the user.
202    ///
203    /// # Errors
204    ///
205    /// Returns [`Error::Http`] if the user that made the reaction is unable to be retrieved from
206    /// the API.
207    pub async fn user(&self, cache_http: impl CacheHttp) -> Result<User> {
208        if let Some(id) = self.user_id {
209            id.to_user(cache_http).await
210        } else {
211            // This can happen if only Http was passed to Message::react, even though
212            // "cache" was enabled.
213            #[cfg(feature = "cache")]
214            {
215                if let Some(cache) = cache_http.cache() {
216                    return Ok(cache.current_user().clone().into());
217                }
218            }
219
220            Ok(cache_http.http().get_current_user().await?.into())
221        }
222    }
223
224    /// Retrieves the list of [`User`]s who have reacted to a [`Message`] with a certain [`Emoji`].
225    ///
226    /// The default `limit` is `50` - specify otherwise to receive a different maximum number of
227    /// users. The maximum that may be retrieve at a time is `100`, if a greater number is provided
228    /// then it is automatically reduced.
229    ///
230    /// The optional `after` attribute is to retrieve the users after a certain user. This is
231    /// useful for pagination.
232    ///
233    /// Requires the [Read Message History] permission.
234    ///
235    /// **Note**: This will send a request to the REST API.
236    ///
237    /// # Errors
238    ///
239    /// Returns a [`ModelError::InvalidPermissions`] if the current user does not have the required
240    /// [permissions].
241    ///
242    /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY
243    /// [permissions]: crate::model::permissions
244    #[inline]
245    pub async fn users<R, U>(
246        &self,
247        http: impl AsRef<Http>,
248        reaction_type: R,
249        limit: Option<u8>,
250        after: Option<U>,
251    ) -> Result<Vec<User>>
252    where
253        R: Into<ReactionType>,
254        U: Into<UserId>,
255    {
256        self.users_(http, &reaction_type.into(), limit, after.map(Into::into)).await
257    }
258
259    async fn users_(
260        &self,
261        http: impl AsRef<Http>,
262        reaction_type: &ReactionType,
263        limit: Option<u8>,
264        after: Option<UserId>,
265    ) -> Result<Vec<User>> {
266        let mut limit = limit.unwrap_or(50);
267
268        if limit > 100 {
269            limit = 100;
270            warn!("Reaction users limit clamped to 100! (API Restriction)");
271        }
272
273        http.as_ref()
274            .get_reaction_users(
275                self.channel_id,
276                self.message_id,
277                reaction_type,
278                limit,
279                after.map(UserId::get),
280            )
281            .await
282    }
283}
284
285/// The type of a [`Reaction`] sent.
286#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
287#[derive(Clone, Debug, Eq, PartialEq, Hash)]
288#[non_exhaustive]
289pub enum ReactionType {
290    /// A reaction with a [`Guild`]s custom [`Emoji`], which is unique to the guild.
291    Custom {
292        /// Whether the emoji is animated.
293        animated: bool,
294        /// The Id of the custom [`Emoji`].
295        id: EmojiId,
296        /// The name of the custom emoji. This is primarily used for decoration and distinguishing
297        /// the emoji client-side.
298        name: Option<String>,
299    },
300    /// A reaction with a twemoji.
301    Unicode(String),
302}
303
304// Manual impl needed to decide enum variant by presence of `id`
305impl<'de> Deserialize<'de> for ReactionType {
306    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
307        #[derive(Deserialize)]
308        struct PartialEmoji {
309            #[serde(default)]
310            animated: bool,
311            id: Option<EmojiId>,
312            name: Option<String>,
313        }
314        let emoji = PartialEmoji::deserialize(deserializer)?;
315        Ok(match (emoji.id, emoji.name) {
316            (Some(id), name) => ReactionType::Custom {
317                animated: emoji.animated,
318                id,
319                name,
320            },
321            (None, Some(name)) => ReactionType::Unicode(name),
322            (None, None) => return Err(DeError::custom("invalid reaction type data")),
323        })
324    }
325}
326
327impl Serialize for ReactionType {
328    fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
329    where
330        S: Serializer,
331    {
332        match self {
333            ReactionType::Custom {
334                animated,
335                id,
336                name,
337            } => {
338                let mut map = serializer.serialize_map(Some(3))?;
339
340                map.serialize_entry("animated", animated)?;
341                map.serialize_entry("id", id)?;
342                map.serialize_entry("name", name)?;
343
344                map.end()
345            },
346            ReactionType::Unicode(name) => {
347                let mut map = serializer.serialize_map(Some(1))?;
348
349                map.serialize_entry("name", name)?;
350
351                map.end()
352            },
353        }
354    }
355}
356
357impl ReactionType {
358    /// Creates a data-esque display of the type. This is not very useful for displaying, as the
359    /// primary client can not render it, but can be useful for debugging.
360    ///
361    /// **Note**: This is mainly for use internally. There is otherwise most likely little use for
362    /// it.
363    #[inline]
364    #[must_use]
365    #[cfg(feature = "http")]
366    pub fn as_data(&self) -> String {
367        match self {
368            ReactionType::Custom {
369                id,
370                name,
371                ..
372            } => {
373                format!("{}:{id}", name.as_deref().unwrap_or_default())
374            },
375            ReactionType::Unicode(unicode) => {
376                utf8_percent_encode(unicode, NON_ALPHANUMERIC).to_string()
377            },
378        }
379    }
380
381    /// Helper function to allow testing equality of unicode emojis without having to perform any
382    /// allocation. Will always return false if the reaction was not a unicode reaction.
383    #[must_use]
384    pub fn unicode_eq(&self, other: &str) -> bool {
385        if let ReactionType::Unicode(unicode) = &self {
386            unicode == other
387        } else {
388            // Always return false if not a unicode reaction
389            false
390        }
391    }
392
393    /// Helper function to allow comparing unicode emojis without having to perform any allocation.
394    /// Will return None if the reaction was not a unicode reaction.
395    #[must_use]
396    pub fn unicode_partial_cmp(&self, other: &str) -> Option<Ordering> {
397        if let ReactionType::Unicode(unicode) = &self {
398            Some(unicode.as_str().cmp(other))
399        } else {
400            // Always return None if not a unicode reaction
401            None
402        }
403    }
404}
405
406impl From<char> for ReactionType {
407    /// Creates a [`ReactionType`] from a `char`.
408    ///
409    /// # Examples
410    ///
411    /// Reacting to a message with an apple:
412    ///
413    /// ```rust,no_run
414    /// # #[cfg(feature = "http")]
415    /// # use serenity::http::CacheHttp;
416    /// # use serenity::model::channel::Message;
417    /// # use serenity::model::id::ChannelId;
418    /// #
419    /// # #[cfg(feature = "http")]
420    /// # async fn example(ctx: impl CacheHttp, message: Message) -> Result<(), Box<dyn std::error::Error>> {
421    /// message.react(ctx, '🍎').await?;
422    /// # Ok(())
423    /// # }
424    /// #
425    /// # fn main() {}
426    /// ```
427    fn from(ch: char) -> ReactionType {
428        ReactionType::Unicode(ch.to_string())
429    }
430}
431
432impl From<Emoji> for ReactionType {
433    fn from(emoji: Emoji) -> ReactionType {
434        ReactionType::Custom {
435            animated: emoji.animated,
436            id: emoji.id,
437            name: Some(emoji.name),
438        }
439    }
440}
441
442impl From<EmojiId> for ReactionType {
443    fn from(emoji_id: EmojiId) -> ReactionType {
444        ReactionType::Custom {
445            animated: false,
446            id: emoji_id,
447            name: Some("emoji".to_string()),
448        }
449    }
450}
451
452impl From<EmojiIdentifier> for ReactionType {
453    fn from(emoji_id: EmojiIdentifier) -> ReactionType {
454        ReactionType::Custom {
455            animated: emoji_id.animated,
456            id: emoji_id.id,
457            name: Some(emoji_id.name),
458        }
459    }
460}
461
462#[derive(Debug)]
463pub struct ReactionConversionError;
464
465impl fmt::Display for ReactionConversionError {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        f.write_str("failed to convert from a string to ReactionType")
468    }
469}
470
471impl std::error::Error for ReactionConversionError {}
472
473impl TryFrom<String> for ReactionType {
474    type Error = ReactionConversionError;
475
476    fn try_from(emoji_string: String) -> std::result::Result<Self, Self::Error> {
477        if emoji_string.is_empty() {
478            return Err(ReactionConversionError);
479        }
480
481        if !emoji_string.starts_with('<') {
482            return Ok(ReactionType::Unicode(emoji_string));
483        }
484        ReactionType::try_from(&emoji_string[..])
485    }
486}
487
488impl TryFrom<&str> for ReactionType {
489    /// Creates a [`ReactionType`] from a string slice.
490    ///
491    /// # Examples
492    ///
493    /// Creating a [`ReactionType`] from a `🍎`, modeling a similar API as the rest of the library:
494    ///
495    /// ```rust
496    /// use std::convert::TryInto;
497    /// use std::fmt::Debug;
498    ///
499    /// use serenity::model::channel::ReactionType;
500    ///
501    /// fn foo<R: TryInto<ReactionType>>(bar: R)
502    /// where
503    ///     R::Error: Debug,
504    /// {
505    ///     println!("{:?}", bar.try_into().unwrap());
506    /// }
507    ///
508    /// foo("🍎");
509    /// ```
510    ///
511    /// Creating a [`ReactionType`] from a custom emoji argument in the following format:
512    ///
513    /// ```rust
514    /// use serenity::model::channel::ReactionType;
515    /// use serenity::model::id::EmojiId;
516    ///
517    /// let emoji_string = "<:customemoji:600404340292059257>";
518    /// let reaction = ReactionType::try_from(emoji_string).unwrap();
519    /// let reaction2 = ReactionType::Custom {
520    ///     animated: false,
521    ///     id: EmojiId::new(600404340292059257),
522    ///     name: Some("customemoji".to_string()),
523    /// };
524    ///
525    /// assert_eq!(reaction, reaction2);
526    /// ```
527    type Error = ReactionConversionError;
528
529    fn try_from(emoji_str: &str) -> std::result::Result<Self, Self::Error> {
530        if emoji_str.is_empty() {
531            return Err(ReactionConversionError);
532        }
533
534        if !emoji_str.starts_with('<') {
535            return Ok(ReactionType::Unicode(emoji_str.to_string()));
536        }
537
538        if !emoji_str.ends_with('>') {
539            return Err(ReactionConversionError);
540        }
541
542        let emoji_str = emoji_str.trim_matches(&['<', '>'] as &[char]);
543
544        let mut split_iter = emoji_str.split(':');
545
546        let animated = split_iter.next().ok_or(ReactionConversionError)? == "a";
547        let name = split_iter.next().ok_or(ReactionConversionError)?.to_string().into();
548        let id = split_iter.next().and_then(|s| s.parse().ok()).ok_or(ReactionConversionError)?;
549
550        Ok(ReactionType::Custom {
551            animated,
552            id,
553            name,
554        })
555    }
556}
557
558impl FromStr for ReactionType {
559    type Err = ReactionConversionError;
560
561    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
562        ReactionType::try_from(s)
563    }
564}
565
566impl fmt::Display for ReactionType {
567    /// Formats the reaction type, displaying the associated emoji in a way that clients can
568    /// understand.
569    ///
570    /// If the type is a [custom][`ReactionType::Custom`] emoji, then refer to the documentation
571    /// for [emoji's formatter][`Emoji::fmt`] on how this is displayed. Otherwise, if the type is a
572    /// [unicode][`ReactionType::Unicode`], then the inner unicode is displayed.
573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574        match self {
575            ReactionType::Custom {
576                animated,
577                id,
578                name,
579            } => {
580                if *animated {
581                    f.write_str("<a:")?;
582                } else {
583                    f.write_str("<:")?;
584                }
585
586                if let Some(name) = name {
587                    f.write_str(name)?;
588                }
589
590                f.write_char(':')?;
591                fmt::Display::fmt(id, f)?;
592                f.write_char('>')
593            },
594            ReactionType::Unicode(unicode) => f.write_str(unicode),
595        }
596    }
597}