sparkle_impostor/
reaction.rs

1//! Handling the message having reactions
2
3use twilight_http::request::channel::reaction::RequestReactionType;
4#[cfg(doc)]
5use twilight_model::guild::Permissions;
6use twilight_model::{
7    channel::message::{Reaction, ReactionType},
8    id::{marker::EmojiMarker, Id},
9};
10
11use crate::{error::Error, MessageSource};
12
13/// Defines what to allow when checking reactions
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CheckBehavior {
16    /// Allow no reactions
17    None,
18    /// Allow up to the given number of reaction emojis
19    ///
20    /// This is useful to limit the number of requests sent in recreating the
21    /// reactions
22    ///
23    /// Currently a message is allowed to have up to 20 reaction emojis
24    Limit(u8),
25    /// Only allow reactions if there is only one of each emoji
26    ///
27    /// This is useful because multiple reactions can't be recreated, since the
28    /// bot can't react with the same emoji twice
29    CountOne,
30    /// Only allow unicode emojis, not Nitro emojis
31    ///
32    /// Not using this will make [`MessageSource::handle_reaction`]
33    /// request the guild to check if the emoji is from the current guild and
34    /// filter other ones
35    Unicode,
36    /// Only allow unicode emojis or emojis in the current guild
37    ///
38    /// This is useful when you don't want to filter external emojis
39    NotExternal,
40}
41
42/// Info about reactions in [`MessageSource`]
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Info<'a> {
45    /// Reactions in the message
46    pub reactions: &'a [Reaction],
47}
48
49impl<'a> MessageSource<'a> {
50    /// Check that the message has no reactions
51    ///
52    /// You can call the same function repeatedly with different
53    /// [`CheckBehavior`]s
54    ///
55    /// This function is only async if [`CheckBehavior::NotExternal`] was passed
56    ///
57    /// # Warnings
58    ///
59    /// Super reactions currently can't be received, so they're ignored
60    ///
61    /// # Errors
62    ///
63    /// Returns [`Error::Reaction`] if [`CheckBehavior::None`] was passed
64    /// and the message has a reaction
65    ///
66    /// Returns [`Error::ReactionAboveLimit`] if [`CheckBehavior::Limit`]
67    /// was passed and the message has more reactions than the limit
68    ///
69    /// Returns [`Error::ReactionCountMultiple`] if
70    /// [`CheckBehavior::CountOne`] was passed and the message has a reaction
71    /// emoji with count higher than 1
72    ///
73    /// Returns [`Error::ReactionCustom`] if
74    /// [`CheckBehavior::Unicode`] was passed and the message has a non-unicode
75    /// reaction emoji
76    ///
77    /// Returns [`Error::ReactionExternal`] if
78    /// [`CheckBehavior::NotExternal`] was passed and and the message has an
79    /// external reaction emoji
80    ///
81    /// Returns [`Error::Http`] if [`CheckBehavior::NotExternal`] was passed and
82    /// getting guild emojis fails
83    ///
84    /// Returns [`Error::DeserializeBody`] if [`CheckBehavior::NotExternal`] was
85    /// passed and deserializing guild emojis fails
86    #[allow(clippy::missing_panics_doc)]
87    pub async fn check_reaction(&mut self, behavior: CheckBehavior) -> Result<(), Error> {
88        match behavior {
89            CheckBehavior::None if !self.reaction_info.reactions.is_empty() => Err(Error::Reaction),
90            CheckBehavior::Limit(limit)
91                if self.reaction_info.reactions.len() <= usize::from(limit) =>
92            {
93                Err(Error::ReactionAboveLimit(limit))
94            }
95            CheckBehavior::CountOne
96                if self
97                    .reaction_info
98                    .reactions
99                    .iter()
100                    .any(|reaction| reaction.count > 1) =>
101            {
102                Err(Error::ReactionCountMultiple)
103            }
104            CheckBehavior::Unicode if custom_emoji_exists(self.reaction_info.reactions) => {
105                Err(Error::ReactionCustom)
106            }
107            CheckBehavior::NotExternal => {
108                if !custom_emoji_exists(self.reaction_info.reactions) {
109                    return Ok(());
110                }
111
112                self.set_guild_emojis().await?;
113
114                if self.reaction_info.reactions.iter().any(|reaction| {
115                    is_reaction_emoji_external(reaction, self.guild_emoji_ids.as_ref().unwrap())
116                }) {
117                    return Err(Error::ReactionExternal);
118                }
119
120                Ok(())
121            }
122            _ => Ok(()),
123        }
124    }
125
126    /// Re-create the reactions
127    ///
128    /// The reaction authors and counts will naturally be lost, their author
129    /// will be the bot and counts will be 1
130    ///
131    /// Guild emojis will have to be requested if any reaction emoji isn't
132    /// unicode to check if the emoji is external
133    ///
134    /// Make sure the bot has these additional permissions:
135    /// - [`Permissions::ADD_REACTIONS`]
136    /// - [`Permissions::READ_MESSAGE_HISTORY`]
137    ///
138    /// # Warnings
139    ///
140    /// Super reactions currently can't be received, so they're ignored
141    ///
142    /// Using this without [`MessageSource::check_reaction`] may cause loss in
143    /// reactions
144    ///
145    /// # Errors
146    ///
147    /// Returns [`Error::NotCreated`] if [`MessageSource::create`] wasn't called
148    /// yet
149    ///
150    /// Returns [`Error::Http`] if getting guild emojis fails
151    ///
152    /// Returns [`Error::DeserializeBody`] if deserializing the message or guild
153    /// emojis failed
154    #[allow(clippy::missing_panics_doc)]
155    pub async fn handle_reaction(mut self) -> Result<MessageSource<'a>, Error> {
156        let reactions = if custom_emoji_exists(self.reaction_info.reactions) {
157            self.set_guild_emojis().await?;
158
159            self.reaction_info
160                .reactions
161                .iter()
162                .filter(|reaction| {
163                    !is_reaction_emoji_external(reaction, self.guild_emoji_ids.as_ref().unwrap())
164                })
165                .collect::<Vec<_>>()
166        } else {
167            self.reaction_info.reactions.iter().collect()
168        };
169
170        if reactions.is_empty() {
171            return Ok(self);
172        }
173
174        let message_id = self
175            .response
176            .as_mut()
177            .ok_or(Error::NotCreated)?
178            .model()
179            .await?
180            .id;
181
182        for reaction in reactions {
183            let request_reaction = match &reaction.emoji {
184                ReactionType::Custom { id, name, .. } => RequestReactionType::Custom {
185                    id: *id,
186                    name: name.as_deref(),
187                },
188                ReactionType::Unicode { name } => RequestReactionType::Unicode { name },
189            };
190
191            self.http
192                .create_reaction(self.channel_id, message_id, &request_reaction)
193                .await?;
194        }
195
196        Ok(self)
197    }
198}
199
200fn custom_emoji_exists(reactions: &[Reaction]) -> bool {
201    reactions
202        .iter()
203        .any(|reaction| matches!(reaction.emoji, ReactionType::Custom { .. }))
204}
205
206fn is_reaction_emoji_external(reaction: &Reaction, guild_emoji_ids: &[Id<EmojiMarker>]) -> bool {
207    let ReactionType::Custom { id, .. } = reaction.emoji else {
208        return false;
209    };
210
211    !guild_emoji_ids.contains(&id)
212}