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}