-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Added an explanation on reactions #55
Open
Strixen
wants to merge
11
commits into
DisnakeDev:main
Choose a base branch
from
Strixen:dev
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
cb17252
feat: Added an explanation on reactions
Strixen 611b8c1
Merge branch 'dev' into dev
Strixen 9c219f4
Update guide/docs/popular-topics/reactions.mdx
Strixen 265d1ce
Update guide/docs/popular-topics/reactions.mdx
Strixen 2b7506f
Update guide/docs/popular-topics/reactions.mdx
Strixen 6a39a2e
Update guide/docs/popular-topics/reactions.mdx
Strixen c210d39
Apply suggestions from code review
Strixen 337719e
Response to Reviews on PR #55
Strixen 031502c
Apply suggestions from code review
Strixen dff3399
Apply suggestions from code review
Strixen 6d79e1a
Spell-Checks
Strixen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
<WorkInProgress /> | ||
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. | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
In Disnake reactions are represented with <DocsLink reference="disnake.Reaction">Reaction</DocsLink> objects. Whenever you operate on a <DocsLink reference="disnake.Message">Message</DocsLink> you can access a list of reactions attached to that message. | ||
In this guide we will be providing an example using the <DocsLink reference="disnake.on_raw_reaction_add">on_raw_reaction_add / remove</DocsLink> events and a message_command's <DocsLink reference="disnake.MessageCommandInteraction">interaction</DocsLink> to demonstrate. | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
:::info | ||
**Reaction limitations** | ||
|
||
- Removing reactions that are not owned by the bot requires <DocsLink reference="disnake.Intents.reactions">Intents.reactions</DocsLink> to be set | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- Therefore <DocsLink reference="disnake.Intents.messages">Intents.messages</DocsLink> is indirectly required if you want to manipulate reactions | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- 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 | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
::: | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<DiscordMessages> | ||
<DiscordMessage profile="user"> | ||
In case you are unaware, reactions are the emojis below this message. | ||
<br /> | ||
Emojis that are highlighted means you've reacted to it, and the number indicates how many have reacted to it. | ||
<div slot="actions"> | ||
<DiscordReactions> | ||
<DiscordReaction | ||
name="disnake_pride" | ||
image="https://cdn.discordapp.com/emojis/983256241142910996.webp?size=96&quality=lossless" | ||
count={3} | ||
active={true} | ||
/> | ||
<DiscordReaction | ||
name="disnake" | ||
image="https://cdn.discordapp.com/emojis/922937443039186975.webp?size=96&quality=lossless" | ||
count={1} | ||
/> | ||
</DiscordReactions> | ||
</div> | ||
</DiscordMessage> | ||
</DiscordMessages> | ||
|
||
### Emojis | ||
|
||
Since reactions utilize Emojis this guide will also include a quick primer on how disnake handles emojis | ||
Emojis have three forms: | ||
|
||
- <DocsLink reference="disnake.Emoji">Emoji</DocsLink> Custom emojis | ||
- <DocsLink reference="disnake.PartialEmoji">PartialEmoji</DocsLink> 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) | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
**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 | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
There is also a small write up about this [`here`](//faq/general.mdx#how-can-i-add-a-reaction-to-a-message) | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
:::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 | ||
::: | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Example using on_reaction events | ||
|
||
There are a few reaction related events we can listen/subscribe to: | ||
|
||
- <DocsLink reference="disnake.on_raw_reaction_add">on_raw_reaction_add</DocsLink> | ||
- <DocsLink reference="disnake.on_raw_reaction_remove">on_raw_reaction_remove</DocsLink> | ||
- <DocsLink reference="disnake.on_raw_reaction_clear">on_raw_reaction_clear</DocsLink> Called when a message has all reactions | ||
removed | ||
- <DocsLink reference="disnake.on_raw_reaction_clear_emoji">on_raw_reaction_clear_emoji</DocsLink> Called when a specific | ||
reaction is removed from a message{' '} | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- More information about events can be found in the docs, [`here`](https://docs.disnake.dev/en/stable/api.html#event-reference) | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
One important thing about raw_reaction events is that all the payloads are only populated with <DocsLink reference="disnake.PartialEmoji">PartialEmojis</DocsLink> | ||
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 <DocsLink reference="disnake.RawReactionActionEvent">RawReactionActionEvent</DocsLink> which is called `payload` in the examples. | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```python title="on_raw_reaction_add.py" | ||
import disnake | ||
|
||
|
||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@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 | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
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 | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<DiscordMessages> | ||
<DiscordMessage profile="user"> | ||
Join the Disnake Discord server, it's an amazing community | ||
<div slot="actions"> | ||
<DiscordReactions> | ||
<DiscordReaction | ||
name="popcorn" | ||
image="https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/160/microsoft/310/popcorn_1f37f.png" | ||
count={1} | ||
/> | ||
<DiscordReaction | ||
name="disnake" | ||
image="https://cdn.discordapp.com/emojis/922937443039186975.webp?size=96&quality=lossless" | ||
count={1} | ||
/> | ||
</DiscordReactions> | ||
</div> | ||
</DiscordMessage> | ||
<DiscordMessage profile="bot"> | ||
Reaction 🍿 added by: AbhigyanTrips! | ||
<div slot="actions"> | ||
<DiscordReactions> | ||
<DiscordReaction | ||
name="popcorn" | ||
image="https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/160/microsoft/310/popcorn_1f37f.png" | ||
count={1} | ||
/> | ||
</DiscordReactions> | ||
</div> | ||
</DiscordMessage> | ||
<DiscordMessage profile="bot">Reaction :disnake: added by: AbhigyanTrips!</DiscordMessage> | ||
</DiscordMessages>{' '} | ||
|
||
:::caution | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
We can only use custom emojis from servers the bot has joined, but we can use them interchangably on those servers. | ||
Bots can make <DocsLink reference="disnake.ui.Button">buttons</DocsLink> using emojis from outside servers they occupy, this may or may not be intended behaviour from Discord and should not be relied on. | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
::: | ||
|
||
Here's a few ways you could filter on reactions to do various things | ||
|
||
```python title=react_actions.py | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
|
||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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. | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- <DocsLink reference="disnake.Reactions.users">disnake.Reactions.users</DocsLink> 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 | ||
shiftinv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# 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 | ||
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
Strixen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TOC will be visible on non-WIP pages.