From cb17252ed347df3589500aa508ce079478efbaa8 Mon Sep 17 00:00:00 2001 From: Thomas Hodnemyr Date: Fri, 6 Jan 2023 16:23:30 +0100 Subject: [PATCH 01/10] feat: Added an explanation on reactions --- guide/docs/popular-topics/reactions.mdx | 299 +++++++++++++++++++++++- 1 file changed, 297 insertions(+), 2 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 59a850ac..48fe330a 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -1,8 +1,303 @@ --- -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 is discords way of adding emojis to other messages. Early on, before discord introduced components; this system was largely used to make interactive messages and apps. +Having bots react to message reactions is less common now, and is somewhat considered legacy behaviour. +This guide will teach you the basics of how they work, since they still have their use cases. +Mainly reaction role systems and polling, but there are some other uses. + +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. + +:::info +**Reaction limitations** + +- Removing reactions that are not owned by the bot requires Intents.reactions to be set +- Therefore Intents.messages is indirectly required if you want to manipulate reactions +- 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 +- Dealing with reactions result in a fair amount of extra api-calls, meaning it can have rate-limit implications on deployment scale. +- Using Reactions as a UX interface was never a inteded behavior, and is ultimatly inferior to the newer component style 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 +- PartialEmoji Stripped down version of Emoji +- [`string`](https://docs.python.org/3/library/string.html) String containing one or more emoji unicodepoints (Emoji modifiers complicates things but thats out of scope) + +**Which one you get is circumstancial:** +Emoji class: is primarely returned when custom emojis are grabbed from the guild/bot +PartialEmoji: are most often custom emojis too, but will usually represent custom emojis the bot can't access +Strings: are normally returned when Unicode CodePoints are used. These are the standard emojis most are familiar with (โœ…๐ŸŽฎ๐Ÿ’›๐Ÿ’ซ) +but these can also come as a PartialEmoji + +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, but can easily be adapted to run inside one. See: [**Creating cogs/extensions**](//getting-started/using-cogs.mdx) +Some examples are also available in the [`DisnakeDev`](https://github.com/DisnakeDev/disnake/tree/master/examples) github repository +::: + +### Example using on_reaction events + +There are a few reaction related events we can listen/subscribe to: + +- on_raw_reaction_add +- on_raw_reaction_remove +- on_raw_reaction_clear Called when a message has all reactions + removed +- on_raw_reaction_clear_emoji Called when a specific + reaction is removed from a message{' '} + +There are non-raw equivilants, 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 preffered, and you are only giving up on an included User/Member object that you can easily fetch if you need it. + +- More information about events can be found in the docs, [`here`](https://docs.disnake.dev/en/stable/api.html#event-reference) + +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 events come a RawReactionActionEvent which is called `payload` in the examples. + +```python title="on_raw_reaction_add.py" +import disnake + + +@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 event's contain few objects, 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 using that stored message, we can add our own reaction to it, and the add_reaction() coroutine supports PartialEmojis so we're good to go + # One thing we need to consider is that the bot cannot access custom emojis outside servers they occupy (see caution below) + # Because of this we need to check if we have access to the custom_emoji. + # disnake.Emoji have a is_usable() function we can 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 could not be found in the cache. + await event_response_message.add_reaction(payload.emoji) +``` + +Below is how the the listener above would react both for a Unicode CodePoint emoji and a custom_emoji the bot can't access +Notice how the **payload.emoji** resolved into **:disnake:** because the emoji is on a server not accessable 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 interchangably on those servers. +Bots can make buttons using emojis from outside servers they occupy, this may or may not be intended behaviour from Discord and should not be relied on. +::: + +Here's a few ways you could filter on reactions to do various things + +```python title=react_actions.py +import disnake + +# These lists are arbitrary and is just to provide context. Using static lists like this can be ok in small bots, but should really be supplied by a db. +allowed_emojis = ["๐Ÿ’™"] +button_emojis = ["โœ…"] +restricted_role_ids = [951263965235773480, 1060778008039919616] +reaction_messages = [1060797825417478154] +reaction_roles = { + "๐ŸŽฎ": 1060778008039919616, + "๐Ÿš€": 1007024363616350308, + "<:catpat:967269162386858055>": 1056775021281943583, +} + + +@bot.listen() +async def on_raw_reaction_add(payload: disnake.RawReactionActionEvent): + # This example is more to illustrate the different ways you can filter which emoji's are used and then you can do your actions on them + # All the functions in it have been tested, but you should add more checks and returns if you actually wanted all these functions at the same time + + # 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 + + # Again, 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) + + # Members with a restricted role, are only allowed to react with๐Ÿ’™ -- From the docs we know that str(PartialEmoji) returns either the codepoint or <:emoji:id> + if [role for role in payload.member.roles if role.id in restricted_role_ids] and not str( + payload.emoji + ) in allowed_emojis: + # Since the list did not return empty and is not a allowed emoji, we remove it + await event_message.remove_reaction(emoji=payload.emoji, member=payload.member) + + # Similar behavior can be useful if you want to use reactions as buttons. Since you have to un-react and react again to repeat the effect + # This can be usefull if you want the functionality of buttons, but want a more compact look. + # but its also a lot of extra api calls compared to components + if str(payload.emoji) in button_emojis: + # In a proper bot you would do more checks, against message_id most likely. + # As theese reactions might normaly be supplied by the bot in the first place + + # Or if the member has the right roles in case the reaction has a moderation function for instance + # Otherwise the awesome_function() can end up going off at wrong places + await event_message.remove_reaction( + emoji=payload.emoji, member=payload.member + ) # Remove the reaction + awesome_function() + await event_channel.send("Done!", delete_after=10.0) + # Short message to let the user know it went ok. This is not an interaction so a message response is not strictly needed + + # A very simple reaction role system + if str(payload.emoji) in reaction_roles.keys() and payload.message_id in reaction_messages: + role_to_apply = bot.get_guild(payload.guild_id).get_role(reaction_roles[str(payload.emoji)]) + if ( + role_to_apply and not role_to_apply in payload.member.roles + ): # Check if we actually got a role, then check if the member already has it, if not add it + await payload.member.add_roles(role_to_apply) + + +@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 + + # Counterpart to the simple reaction role system + if ( + str(payload.emoji) in reaction_roles.keys() and payload.message_id in reaction_messages + ): # Check that the emoji and message is correct + role_to_remove = bot.get_guild(payload.guild_id).get_role( + reaction_roles[str(payload.emoji)] + ) + if ( + role_to_apply and role_to_apply in payload.member.roles + ): # Check if we actually got a role, then check if the member actually has it, then remove it + await payload.member.remove_roles(role_to_apply) +``` + +### Example using message_command + +We could go with a `slash_command` here, but since we will be targeting other messages, it adds a complication because if the message is in a different channel from where the command is executed; the retrieved message will be `None`. +Using a `message_command` instead side-steps this issue, since the targeted message will be present in the interaction object. + +- disnake.Reactions.users won't be covered here since the docs + demonstrate its use elegantly. + +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): + + # Here's a very pythonic way of making a list of the reactions + response_string = ( + "".join( + [ + f"{index+1}. {reaction.emoji} - {reaction.count}\n" + for index, reaction in enumerate(inter.target.reactions) + ] + ) + or "No Reactions found" + ) + + # Here it is broken up in case list comprehensions are too confusing + 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 ( + not response_string + ): # If the message has no reactions, response_string will be "" which evaluates as False + response_string = "No Reactions found" + + await inter.response.send_message(response_string) + + # 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 tho 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. + # When listing through reactions 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) +``` From 9c219f4c25ea20985b334f581ba652379572494c Mon Sep 17 00:00:00 2001 From: Strix Date: Sat, 7 Jan 2023 07:54:48 +0100 Subject: [PATCH 02/10] Update guide/docs/popular-topics/reactions.mdx Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 48fe330a..dd01063a 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -5,7 +5,7 @@ hide_table_of_contents: true # Reactions -Reactions is discords way of adding emojis to other messages. Early on, before discord introduced components; this system was largely used to make interactive messages and apps. +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 message reactions is less common now, and is somewhat considered legacy behaviour. This guide will teach you the basics of how they work, since they still have their use cases. Mainly reaction role systems and polling, but there are some other uses. From 265d1ce2e41b5a8d79e274742858a6819d857568 Mon Sep 17 00:00:00 2001 From: Strix Date: Sat, 7 Jan 2023 07:55:32 +0100 Subject: [PATCH 03/10] Update guide/docs/popular-topics/reactions.mdx Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index dd01063a..103a1a5a 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -6,9 +6,8 @@ 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 message reactions is less common now, and is somewhat considered legacy behaviour. -This guide will teach you the basics of how they work, since they still have their use cases. -Mainly reaction role systems and polling, but there are some other uses. +Having bots react to messages is less common now, and is somewhat considered legacy behaviour. +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. From 2b7506ff3a7a695a5dda48a46b74d089f5b13871 Mon Sep 17 00:00:00 2001 From: Strix Date: Sat, 7 Jan 2023 07:56:01 +0100 Subject: [PATCH 04/10] Update guide/docs/popular-topics/reactions.mdx Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 103a1a5a..46221440 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -9,8 +9,8 @@ Reactions are Discord's way of adding emojis to other messages. Early on, before Having bots react to messages is less common now, and is somewhat considered legacy behaviour. 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. +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. :::info **Reaction limitations** From 6a39a2ef5cc112a5b21a4ecc2b4035678efd753b Mon Sep 17 00:00:00 2001 From: Strix Date: Sat, 7 Jan 2023 07:56:50 +0100 Subject: [PATCH 05/10] Update guide/docs/popular-topics/reactions.mdx Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 46221440..1cbfdf0d 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -21,7 +21,7 @@ In this guide we will be providing an example using the From c210d394cc45e4221c43225654bc44d88c02ee20 Mon Sep 17 00:00:00 2001 From: Strix Date: Sat, 7 Jan 2023 09:03:04 +0100 Subject: [PATCH 06/10] Apply suggestions from code review These are good suggestions Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 62 +++++++++++-------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 1cbfdf0d..2516c78c 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -18,9 +18,9 @@ In this guide we will be providing an example using the Intents.reactions to be set - Therefore Intents.messages is indirectly required if you want to manipulate reactions - 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 -- Dealing with reactions result in a fair amount of extra api-calls, meaning it can have rate-limit implications on deployment scale. -- Using Reactions as a UX interface was never a inteded behavior, and is ultimatly inferior to the newer component style interface +- Reactions are inherently linked to emojis, and your bot will not have access to resend all emojis used by Discord users. +- Dealing with reactions results in a fair amount of extra API calls, meaning it can have rate-limit implications on deployment scale. +- Using Reactions as a UX interface was never a intended behavior, and is ultimately inferior to the newer component style interface. ::: @@ -61,27 +61,25 @@ PartialEmoji: are most often custom emojis too, but will usually represent custo Strings: are normally returned when Unicode CodePoints are used. These are the standard emojis most are familiar with (โœ…๐ŸŽฎ๐Ÿ’›๐Ÿ’ซ) but these can also come as a PartialEmoji -There is also a small write up about this [`here`](//faq/general.mdx#how-can-i-add-a-reaction-to-a-message) +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, but can easily be adapted to run inside one. See: [**Creating cogs/extensions**](//getting-started/using-cogs.mdx) -Some examples are also available in the [`DisnakeDev`](https://github.com/DisnakeDev/disnake/tree/master/examples) github repository +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 we can listen/subscribe to: -- on_raw_reaction_add -- on_raw_reaction_remove -- on_raw_reaction_clear Called when a message has all reactions - removed -- on_raw_reaction_clear_emoji Called when a specific - reaction is removed from a message{' '} +- 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 equivilants, 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 preffered, and you are only giving up on an included User/Member object that you can easily fetch if you need it. +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. - More information about events can be found in the docs, [`here`](https://docs.disnake.dev/en/stable/api.html#event-reference) @@ -90,9 +88,6 @@ This is generally not an issue since it contains everything we need, but its som Raw reaction events come a RawReactionActionEvent which is called `payload` in the examples. ```python title="on_raw_reaction_add.py" -import disnake - - @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 @@ -103,7 +98,7 @@ async def on_raw_reaction_add(self, payload: disnake.RawReactionActionEvent): if not payload.guild_id: return # guild_id is None if its a DM - # Raw event's contain few objects, so we need to grab the channel from the cache + # 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 @@ -111,17 +106,17 @@ async def on_raw_reaction_add(self, payload: disnake.RawReactionActionEvent): content=f"Reaction {payload.emoji} added by: {payload.member.display_name}!" ) - # Now using that stored message, we can add our own reaction to it, and the add_reaction() coroutine supports PartialEmojis so we're good to go - # One thing we need to consider is that the bot cannot access custom emojis outside servers they occupy (see caution below) - # Because of this we need to check if we have access to the custom_emoji. - # disnake.Emoji have a is_usable() function we can 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 could not be found in the cache. + # 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 CodePoint emoji and a custom_emoji the bot can't access -Notice how the **payload.emoji** resolved into **:disnake:** because the emoji is on a server not accessable to the bot +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: @@ -154,11 +149,13 @@ Notice how the **payload.emoji** resolved into **:disnake:** because the emoji i Reaction :disnake: added by: AbhigyanTrips! -{' '} + + +
:::caution We can only use custom emojis from servers the bot has joined, but we can use them interchangably on those servers. -Bots can make buttons using emojis from outside servers they occupy, this may or may not be intended behaviour from Discord and should not be relied on. +Bots can make buttons using emojis from servers they're not members of, this may or may not be intended behaviour by Discord and should not be relied on. ::: Here's a few ways you could filter on reactions to do various things @@ -277,12 +274,9 @@ async def list_reactions(self, inter: disnake.MessageCommandInteraction): 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 ( - not response_string - ): # If the message has no reactions, response_string will be "" which evaluates as False - response_string = "No Reactions found" - await inter.response.send_message(response_string) + # 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 From 337719e27f3908b8fca2b05856c3a1000e32044f Mon Sep 17 00:00:00 2001 From: Thomas Hodnemyr Date: Sat, 7 Jan 2023 10:42:51 +0100 Subject: [PATCH 07/10] Response to Reviews on PR #55 Merged the suggesions from Shiftinv and took their comments into consideration and made some alterations --- guide/docs/popular-topics/reactions.mdx | 203 +++++++++++++----------- 1 file changed, 114 insertions(+), 89 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 2516c78c..88cbe599 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -5,22 +5,25 @@ 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. +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 behaviour. 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** -- Removing reactions that are not owned by the bot requires Intents.reactions to be set -- Therefore Intents.messages is indirectly required if you want to manipulate reactions +- 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. +- 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 as a UX interface was never a intended behavior, and is ultimately inferior to the newer component style interface. + ::: @@ -49,17 +52,14 @@ In this guide we will be providing an example using the Emoji Custom emojis -- PartialEmoji Stripped down version of Emoji -- [`string`](https://docs.python.org/3/library/string.html) String containing one or more emoji unicodepoints (Emoji modifiers complicates things but thats out of scope) - -**Which one you get is circumstancial:** -Emoji class: is primarely returned when custom emojis are grabbed from the guild/bot -PartialEmoji: are most often custom emojis too, but will usually represent custom emojis the bot can't access -Strings: are normally returned when Unicode CodePoints are used. These are the standard emojis most are familiar with (โœ…๐ŸŽฎ๐Ÿ’›๐Ÿ’ซ) -but these can also come as a PartialEmoji +- Emoji Custom emojis are primarely 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). @@ -71,21 +71,23 @@ Some examples are also available in the [GitHub repository](https://github.com/D ### Example using on_reaction events -There are a few reaction related events we can listen/subscribe to: +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 +- 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. -- More information about events can be found in the docs, [`here`](https://docs.disnake.dev/en/stable/api.html#event-reference) - 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 events come a RawReactionActionEvent which is called `payload` in the examples. +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() @@ -151,75 +153,112 @@ Notice how second emoji resolved into **:disnake:** because the emoji is on a se Reaction :disnake: added by: AbhigyanTrips! -
+
:::caution We can only use custom emojis from servers the bot has joined, but we can use them interchangably on those servers. Bots can make buttons using emojis from servers they're not members of, this may or may not be intended behaviour by Discord and should not be relied on. ::: -Here's a few ways you could filter on reactions to do various things +
-```python title=react_actions.py -import disnake +**Here are a few examples on how reactions can be implemented:** -# These lists are arbitrary and is just to provide context. Using static lists like this can be ok in small bots, but should really be supplied by a db. + + + +```python allowed_emojis = ["๐Ÿ’™"] -button_emojis = ["โœ…"] restricted_role_ids = [951263965235773480, 1060778008039919616] -reaction_messages = [1060797825417478154] -reaction_roles = { - "๐ŸŽฎ": 1060778008039919616, - "๐Ÿš€": 1007024363616350308, - "<:catpat:967269162386858055>": 1056775021281943583, -} @bot.listen() async def on_raw_reaction_add(payload: disnake.RawReactionActionEvent): - # This example is more to illustrate the different ways you can filter which emoji's are used and then you can do your actions on them - # All the functions in it have been tested, but you should add more checks and returns if you actually wanted all these functions at the same time - # 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 - # Again, getting the channel, and fetching message as these will be useful + # 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) - # Members with a restricted role, are only allowed to react with๐Ÿ’™ -- From the docs we know that str(PartialEmoji) returns either the codepoint or <:emoji:id> + # Members with a restricted role, are only allowed to react with ๐Ÿ’™ -- From the docs we know that str(PartialEmoji) returns either the codepoint or <:emoji:id> if [role for role in payload.member.roles if role.id in restricted_role_ids] and not str( payload.emoji ) in allowed_emojis: # Since the list did not return empty and is not a allowed emoji, we remove it await event_message.remove_reaction(emoji=payload.emoji, member=payload.member) +``` + + + + + +```python +# Since you can to un-react for the user we can emulate a button +# This can be usefull 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 not payload.guild_id: + return + if payload.channel_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) + + await event_message.remove_reaction( + emoji=payload.emoji, member=payload.member + ) # Remove the reaction + awesome_function() # Do some stuff + await event_channel.send("Done!", delete_after=10.0) + # Short message to let the user know it went ok. This is not an interaction so a message response is not strictly needed +``` + + + - # Similar behavior can be useful if you want to use reactions as buttons. Since you have to un-react and react again to repeat the effect - # This can be usefull if you want the functionality of buttons, but want a more compact look. - # but its also a lot of extra api calls compared to components - if str(payload.emoji) in button_emojis: - # In a proper bot you would do more checks, against message_id most likely. - # As theese reactions might normaly be supplied by the bot in the first place - - # Or if the member has the right roles in case the reaction has a moderation function for instance - # Otherwise the awesome_function() can end up going off at wrong places - await event_message.remove_reaction( - emoji=payload.emoji, member=payload.member - ) # Remove the reaction - awesome_function() - await event_channel.send("Done!", delete_after=10.0) - # Short message to let the user know it went ok. This is not an interaction so a message response is not strictly needed - - # A very simple reaction role system - if str(payload.emoji) in reaction_roles.keys() and payload.message_id in reaction_messages: - role_to_apply = bot.get_guild(payload.guild_id).get_role(reaction_roles[str(payload.emoji)]) - if ( - role_to_apply and not role_to_apply in payload.member.roles - ): # Check if we actually got a role, then check if the member already has it, if not add it - await payload.member.add_roles(role_to_apply) +```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 + if ( + str(payload.emoji) not in reaction_roles.keys() + or payload.message_id not in reaction_messages + ): + return + + role_to_apply = bot.get_guild(payload.guild_id).get_role(reaction_roles[str(payload.emoji)]) + if ( + role_to_apply and not role_to_apply in payload.member.roles + ): # Check if we actually got a role, then check if the member already has it, if not add it + await payload.member.add_roles(role_to_apply) @bot.listen() @@ -228,46 +267,32 @@ async def on_raw_reaction_remove(payload: disnake.RawReactionActionEvent): return if not payload.guild_id: return # guild_id is None if its a DM + if ( + str(payload.emoji) not in reaction_roles.keys() + or payload.message_id not in reaction_messages + ): + return - # Counterpart to the simple reaction role system + role_to_remove = bot.get_guild(payload.guild_id).get_role(reaction_roles[str(payload.emoji)]) if ( - str(payload.emoji) in reaction_roles.keys() and payload.message_id in reaction_messages - ): # Check that the emoji and message is correct - role_to_remove = bot.get_guild(payload.guild_id).get_role( - reaction_roles[str(payload.emoji)] - ) - if ( - role_to_apply and role_to_apply in payload.member.roles - ): # Check if we actually got a role, then check if the member actually has it, then remove it - await payload.member.remove_roles(role_to_apply) + role_to_apply and role_to_apply in payload.member.roles + ): # Check if we actually got a role, then check if the member actually has it, then remove it + await payload.member.remove_roles(role_to_apply) ``` -### Example using message_command + -We could go with a `slash_command` here, but since we will be targeting other messages, it adds a complication because if the message is in a different channel from where the command is executed; the retrieved message will be `None`. -Using a `message_command` instead side-steps this issue, since the targeted message will be present in the interaction object. + -- disnake.Reactions.users won't be covered here since the docs - demonstrate its use elegantly. +### 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): - # Here's a very pythonic way of making a list of the reactions - response_string = ( - "".join( - [ - f"{index+1}. {reaction.emoji} - {reaction.count}\n" - for index, reaction in enumerate(inter.target.reactions) - ] - ) - or "No Reactions found" - ) - - # Here it is broken up in case list comprehensions are too confusing 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( @@ -289,7 +314,7 @@ async def list_reactions(self, inter: disnake.MessageCommandInteraction): await inter.target.add_reaction(reaction) # However we still cannot add new reactions we don't have access to. - # When listing through reactions PartialEmojis are generated if the bot does not have access to it, so we can filter on that to skip them + # 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) From 031502c2e9f20b755747cd2bf629837dda973909 Mon Sep 17 00:00:00 2001 From: Strix Date: Sat, 7 Jan 2023 16:27:01 +0100 Subject: [PATCH 08/10] Apply suggestions from code review LGTM Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 88cbe599..86a1a8fa 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -18,11 +18,11 @@ In this guide we will be providing an example using the Intents.reactions is recommended to manipulate others reactions, and is required if you intend to utilize events. +- 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 as a UX interface was never a intended behavior, and is ultimately inferior to the newer component style interface. +- Using Reactions for user interfaces was never intended behavior, and is ultimately inferior to the newer component interface. ::: @@ -310,7 +310,7 @@ async def list_reactions(self, inter: disnake.MessageCommandInteraction): for reaction in reaction_list: - # Since the reactions are present on the message, the bot can react to it, even tho it does not have access to the custom emoji + # 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. From dff3399922bd667c04d33bea4c7bc6459e0ad026 Mon Sep 17 00:00:00 2001 From: Strix Date: Sun, 8 Jan 2023 14:54:47 +0100 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Strix --- guide/docs/popular-topics/reactions.mdx | 69 +++++++++++-------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index 86a1a8fa..c8dfb107 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -168,27 +168,28 @@ Bots can make buttons using e ```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 - # 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) + # 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) - # Members with a restricted role, are only allowed to react with ๐Ÿ’™ -- From the docs we know that str(PartialEmoji) returns either the codepoint or <:emoji:id> - if [role for role in payload.member.roles if role.id in restricted_role_ids] and not str( - payload.emoji - ) in allowed_emojis: - # Since the list did not return empty and is not a allowed emoji, we remove it await event_message.remove_reaction(emoji=payload.emoji, member=payload.member) ``` @@ -197,8 +198,8 @@ async def on_raw_reaction_add(payload: disnake.RawReactionActionEvent): ```python -# Since you can to un-react for the user we can emulate a button -# This can be usefull if you want the functionality of buttons, but want a more compact look. +# 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 @@ -206,24 +207,21 @@ 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 not payload.guild_id: - return - if payload.channel_id not in reaction_messages or str(payload.emoji) not in button_emojis: + 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) - await event_message.remove_reaction( - emoji=payload.emoji, member=payload.member - ) # Remove the reaction + # Remove the reaction + await event_message.remove_reaction(emoji=payload.emoji, member=payload.member) awesome_function() # Do some stuff - await event_channel.send("Done!", delete_after=10.0) + # 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) ``` @@ -242,23 +240,20 @@ reaction_roles = { @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 - if ( - str(payload.emoji) not in reaction_roles.keys() - or payload.message_id not in reaction_messages - ): + + role_id = reaction_roles.get(str(payload.emoji)) + if payload.message_id not in reaction_messages or not role_id: return - role_to_apply = bot.get_guild(payload.guild_id).get_role(reaction_roles[str(payload.emoji)]) - if ( - role_to_apply and not role_to_apply in payload.member.roles - ): # Check if we actually got a role, then check if the member already has it, if not add it - await payload.member.add_roles(role_to_apply) + 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() @@ -267,17 +262,15 @@ async def on_raw_reaction_remove(payload: disnake.RawReactionActionEvent): return if not payload.guild_id: return # guild_id is None if its a DM - if ( - str(payload.emoji) not in reaction_roles.keys() - or payload.message_id not in reaction_messages - ): + + role_id = reaction_roles.get(str(payload.emoji)) + if payload.message_id not in reaction_messages or not role_id: return - role_to_remove = bot.get_guild(payload.guild_id).get_role(reaction_roles[str(payload.emoji)]) - if ( - role_to_apply and role_to_apply in payload.member.roles - ): # Check if we actually got a role, then check if the member actually has it, then remove it - await payload.member.remove_roles(role_to_apply) + 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) ``` From 6d79e1a184f040988d84e96bbca711481bf796f9 Mon Sep 17 00:00:00 2001 From: Thomas Hodnemyr Date: Sun, 12 Feb 2023 15:22:14 +0100 Subject: [PATCH 10/10] Spell-Checks Fixed some straggling spelling errors --- guide/docs/popular-topics/reactions.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guide/docs/popular-topics/reactions.mdx b/guide/docs/popular-topics/reactions.mdx index c8dfb107..6b06623a 100644 --- a/guide/docs/popular-topics/reactions.mdx +++ b/guide/docs/popular-topics/reactions.mdx @@ -6,7 +6,7 @@ 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 behaviour. +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. @@ -54,7 +54,7 @@ In this guide we will be providing an example using the Emoji Custom emojis are primarely returned when custom emojis are grabbed +- 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 @@ -156,8 +156,8 @@ Notice how second emoji resolved into **:disnake:** because the emoji is on a se
:::caution -We can only use custom emojis from servers the bot has joined, but we can use them interchangably on those servers. -Bots can make buttons using emojis from servers they're not members of, this may or may not be intended behaviour by Discord and should not be relied on. +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. :::