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}