diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 59a850ac..6b06623a 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -1,8 +1,314 @@ --- -description: Create polls, paginate your commands, and more. +description: This section covers the topic of interacting with message reactions. Both adding them with code, and reacting to on_reaction events hide_table_of_contents: true --- # Reactions - +Reactions are Discord's way of adding emojis to other messages. Early on, before Discord introduced [components](../interactions/buttons.mdx), this system was largely used to make interactive messages and apps. +Having bots react to messages is less common now, and is somewhat considered legacy behavior. +This guide will teach you the basics of how they work, since they still have their use cases, like reaction role systems and polling. + +In Disnake, reactions are represented with Reaction objects. Whenever you operate on a Message you can access a list of reactions attached to that message. +In this guide we will be providing an example using the on_raw_reaction_add / remove events and a message_command's interaction to demonstrate. + +- disnake.Reactions.users won't be covered here since the docs + demonstrate its use elegantly. + +:::info +**Reaction limitations** + +- To maintain a consistent reaction cache, Intents.reactions is recommended to manipulate others' reactions, and is required if you intend to utilize events. +- A message can have a maximum of 20 unique reactions on it at one time. +- Reactions are inherently linked to emojis, and your bot will not have access to resend all emojis used by Discord users. ( The bot can always react to others reactions ) +- Dealing with reactions results in a fair amount of extra API calls, meaning it can have rate-limit implications on deployment scale. +- Using Reactions for user interfaces was never intended behavior, and is ultimately inferior to the newer component interface. + +::: + + + + In case you are unaware, reactions are the emojis below this message. +
+ Emojis that are highlighted means you've reacted to it, and the number indicates how many have reacted to it. +
+ + + + +
+
+
+ +### Emojis + +Since reactions utilize Emojis this guide will also include a quick primer on how disnake handles emojis +**Emojis have three forms:** + +- Emoji Custom emojis are primarily returned when custom emojis are grabbed + from the guild/bot +- PartialEmoji Stripped down version of Emoji. Which appears in raw + events or when the bot cannot access the custom emoji +- [`string`](https://docs.python.org/3/library/string.html) Strings: are normally returned when unicode emojis are used. These are the standard emojis most are familiar with (✅🎮💛💫) + but these will also come as a PartialEmoji in raw events + +There is also a small write up about this [here](../faq/general.mdx#how-can-i-add-a-reaction-to-a-message). + +:::note +The examples are only meant to demonstrate how disnake interacts with reactions, and should probably not be copied verbatim. +These examples are not intended for [cogs](../getting-started/using-cogs.mdx), but can easily be adapted to run inside them. +Some examples are also available in the [GitHub repository](https://github.com/DisnakeDev/disnake/tree/master/examples). +::: + +### Example using on_reaction events + +There are a few reaction related [events](https://docs.disnake.dev/en/stable/api.html#event-reference) we can listen/subscribe to: + +- on_raw_reaction_add, called when a user adds a reaction +- on_raw_reaction_remove, called when a user's + reaction is removed +- on_raw_reaction_clear, called when a message has all + reactions removed +- on_raw_reaction_clear_emoji, called when all + reactions with a specific emoji are removed from a message + +There are non-raw equivalents, but they rely on the cache. If the message is not found in the internal cache, then the event is not called. +For this reason raw events are preferred, and you are only giving up on an included User/Member object that you can easily fetch if you need it. + +One important thing about raw_reaction events is that all the payloads are only populated with PartialEmojis +This is generally not an issue since it contains everything we need, but its something you should be aware of. +Raw reaction add/remove events come as RawReactionActionEvent which is called `payload` in the examples. +The raw clearing events each have their own event payloads. + +```python title="on_raw_reaction_add.py" +@bot.listen() +async def on_raw_reaction_add(self, payload: disnake.RawReactionActionEvent): + # For this example we will have the bot post a message describing the event, and adding the emoji to that message as an exercise + + # We don't want the bot to react to its own actions, nor DM's in this case + if payload.user_id == bot.user.id: + return + if not payload.guild_id: + return # guild_id is None if its a DM + + # Raw events contain the channel ID, so we need to grab the channel from the cache + event_channel = bot.get_channel(payload.channel_id) + + # With the channel in hand we can use it to post a new message like normal, Messageable.send() returns the message, and we need to store it + event_response_message = await event_channel.send( + content=f"Reaction {payload.emoji} added by: {payload.member.display_name}!" + ) + + # Now, we could add our own reaction to the message we just sent. + # One thing we need to consider is that the bot cannot access custom emojis from servers they're not members of (see caution below), + # because of this we need to check if we have access to the custom_emoji. + # disnake.Emoji has a `is_usable()` function we could reference, but partials do not, so we need to check manually. + if payload.emoji.is_custom_emoji() and not bot.get_emoji(payload.emoji.id): + return # The emoji is custom, but from a guild the bot cannot access. + await event_response_message.add_reaction(payload.emoji) +``` + +Below is how the the listener above would react both for a unicode emoji and a custom emoji the bot can't access. +Notice how second emoji resolved into **:disnake:** because the emoji is on a server not accessible to the bot: + + + + Join the Disnake Discord server, it's an amazing community +
+ + + + +
+
+ + Reaction 🍿 added by: AbhigyanTrips! +
+ + + +
+
+ Reaction :disnake: added by: AbhigyanTrips! +
+ +
+ +:::caution +We can only use custom emojis from servers the bot has joined, but we can use them interchangeably on those servers. +Bots can make buttons using emojis from servers they're not members of, this may or may not be intended behavior by Discord and should not be relied on. +::: + +
+ +**Here are a few examples on how reactions can be implemented:** + + + + +```python +# Members with a restricted role are only allowed to react with 💙 + +allowed_emojis = ["💙"] +restricted_role_ids = [951263965235773480, 1060778008039919616] + + +@bot.listen() +async def on_raw_reaction_add(payload: disnake.RawReactionActionEvent): + if payload.user_id == bot.user.id: + return + if not payload.guild_id: + return # guild_id is None if its a DM + + # From the docs we know that str(PartialEmoji) returns either the codepoint or <:emoji:id> + if ( + any(payload.member.get_role(role) for role in restricted_role_ids) + and str(payload.emoji) not in allowed_emojis + ): + # Getting the channel, and fetching message as these will be useful + event_channel = bot.get_channel(payload.channel_id) + event_message = await event_channel.fetch_message(payload.message_id) + + await event_message.remove_reaction(emoji=payload.emoji, member=payload.member) +``` + + + + + +```python +# Since you can remove a user's reaction (given appropriate permissions), we can emulate a button. +# This can be useful if you want the functionality of buttons, but want a more compact look. + +button_emojis = ["✅"] # What emojis to react to +reaction_messages = [1060797825417478154] # What messages to monitor + + +@bot.listen() +async def on_raw_reaction_add(payload: disnake.RawReactionActionEvent): + if payload.user_id == bot.user.id: + return + if payload.message_id not in reaction_messages or str(payload.emoji) not in button_emojis: + return + + # Getting the channel, and fetching message as these will be useful + event_channel = bot.get_channel(payload.channel_id) + event_message = await event_channel.fetch_message(payload.message_id) + + # Remove the reaction + await event_message.remove_reaction(emoji=payload.emoji, member=payload.member) + awesome_function() # Do some stuff + + # Short message to let the user know it went ok. This is not an interaction so a message response is not strictly needed + await event_channel.send("Done!", delete_after=10.0) +``` + + + + +```python +# A very simple reaction role system + +reaction_messages = [1060797825417478154] # What messages to monitor +reaction_roles = { + "🎮": 1060778008039919616, + "🚀": 1007024363616350308, + "<:catpat:967269162386858055>": 1056775021281943583, +} # The emojis, and their corresponding role ids + + +@bot.listen() +async def on_raw_reaction_add(payload: disnake.RawReactionActionEvent): + # We usually don't want the bot to react to its own actions, nor DM's in this case + if payload.user_id == bot.user.id: + return + if not payload.guild_id: + return # guild_id is None if its a DM + + role_id = reaction_roles.get(str(payload.emoji)) + if payload.message_id not in reaction_messages or not role_id: + return + + role = bot.get_guild(payload.guild_id).get_role(role_id) + # Check if we actually got a role, then check if the member already has it, if not add it + if role and role not in payload.member.roles: + await payload.member.add_roles(role) + + +@bot.listen() +async def on_raw_reaction_remove(payload: disnake.RawReactionActionEvent): + if payload.user_id == bot.user.id: + return + if not payload.guild_id: + return # guild_id is None if its a DM + + role_id = reaction_roles.get(str(payload.emoji)) + if payload.message_id not in reaction_messages or not role_id: + return + + role = bot.get_guild(payload.guild_id).get_role(role_id) + # Check if we actually got a role, then check if the member actually has it, then remove it + if role and role in payload.member.roles: + await payload.member.remove_roles(role) +``` + + + + + +### Example using an ApplicationCommandInteraction + +Using a `message_command` here because the message object is always included in the message commands interaction instance. +This example is purely to demonstrate using the Reaction object since events deal with a similar but different class + +```python title="message_command.py" +@commands.message_command() +async def list_reactions(self, inter: disnake.MessageCommandInteraction): + + response_string = "" # Start with an empty string + reaction_list = inter.target.reactions # First we get the list of disnake.Reaction objects + for index, reaction in enumerate( + reaction_list + ): # We then loop through the reactions and use enumerate for indexing + response_string += f"{index+1}. {reaction.emoji} - {reaction.count}\n" # Using f-strings we format the list how we like + + # If the message has no reactions, response_string will be "" which evaluates to False + await inter.response.send_message(response_string or "No Reactions found") + + # As with the previous examples, we can add reactions too + + # inter.response.send_message() does not return the message generated so we have to fetch it, thankfully we have this alias we can use + message = await inter.original_response() + + for reaction in reaction_list: + + # Since the reactions are present on the message, the bot can react to it, even though it does not have access to the custom emoji + await inter.target.add_reaction(reaction) + + # However we still cannot add new reactions we don't have access to. + # PartialEmojis are generated if the bot does not have access to it, so we can filter on that to skip them + if isinstance(reaction.emoji, disnake.PartialEmoji): + continue + await message.add_reaction(reaction) +```