From 6bcde9c5b0ef15a74cdbd00e92f57ac6c5c1c1e6 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Wed, 11 Jun 2025 16:05:35 -0600 Subject: [PATCH] Improves reactions documentation with examples and events Expands the reactions guide to include practical examples for adding reactions via message objects, demonstrates low-level client methods, and adds comprehensive event handling examples. Shows how to listen for reaction add/remove events with proper type hints and user filtering to ignore bot reactions. --- docs/reactions.md | 56 ++++++++++++---- examples/reactions.py | 144 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 examples/reactions.py diff --git a/docs/reactions.md b/docs/reactions.md index 40ca754..dba2b43 100644 --- a/docs/reactions.md +++ b/docs/reactions.md @@ -1,32 +1,62 @@ # Handling Reactions -`disagreement` provides simple helpers for adding and removing message reactions. +`disagreement` provides several ways to add, remove, and listen for message reactions. -## HTTP Methods +## Adding & Removing Reactions -Use the `HTTPClient` methods directly if you need lower level control: +The easiest way to add a reaction is to use the helper method on a `Message` object. This is often done within a command context. ```python -await client._http.create_reaction(channel_id, message_id, "👍") -await client._http.delete_reaction(channel_id, message_id, "👍") -users = await client._http.get_reactions(channel_id, message_id, "👍") +# Inside a command function: +# ctx is a commands.CommandContext object +await ctx.message.add_reaction("👍") ``` -You can also use the higher level helpers on :class:`Client`: +You can also remove your own reactions. ```python +await ctx.message.remove_reaction("👍", client.user) +``` + +## Low-Level Control + +For more direct control, you can use methods on the `Client` or `HTTPClient` if you have the channel and message IDs. + +```python +# Using the client helper await client.create_reaction(channel_id, message_id, "👍") -await client.delete_reaction(channel_id, message_id, "👍") + +# Using the raw HTTP method +await client._http.create_reaction(channel_id, message_id, "👍") +``` + +Similarly, you can delete reactions and get a list of users who reacted. + +```python +# Delete a specific user's reaction +await client.delete_reaction(channel_id, message_id, "👍", user_id) + +# Get users who reacted with an emoji users = await client.get_reactions(channel_id, message_id, "👍") ``` ## Reaction Events -Register listeners for `MESSAGE_REACTION_ADD` and `MESSAGE_REACTION_REMOVE`. -Each listener receives a `Reaction` model instance. +Your bot can listen for reaction events by using the `@client.on_event` decorator. The two main events are `MESSAGE_REACTION_ADD` and `MESSAGE_REACTION_REMOVE`. + +The event handlers for these events receive both a `Reaction` object and the `User` or `Member` who triggered the event. ```python +import disagreement +from disagreement.models import Reaction, User, Member + @client.on_event("MESSAGE_REACTION_ADD") -async def on_reaction(reaction: disagreement.Reaction): - print(f"{reaction.user_id} reacted with {reaction.emoji}") -``` +async def on_reaction_add(reaction: Reaction, user: User | Member): + # Ignore reactions from the bot itself + if client.user and user.id == client.user.id: + return + print(f"{user.username} reacted to message {reaction.message_id} with {reaction.emoji}") + +@client.on_event("MESSAGE_REACTION_REMOVE") +async def on_reaction_remove(reaction: Reaction, user: User | Member): + print(f"{user.username} removed their {reaction.emoji} reaction from message {reaction.message_id}") diff --git a/examples/reactions.py b/examples/reactions.py new file mode 100644 index 0000000..6b31363 --- /dev/null +++ b/examples/reactions.py @@ -0,0 +1,144 @@ +# examples/reactions.py + +""" +An example bot demonstrating reaction handling with the Disagreement library. + +This bot will: +1. React to a specific command with a thumbs-up emoji. +2. Log when any reaction is added to a message in a server it's in. +3. Log when any reaction is removed from a message. + +To run this bot: +1. Follow the setup steps in 'basic_bot.py' to set your DISCORD_BOT_TOKEN. +2. Ensure you have the GUILD_MESSAGE_REACTIONS intent enabled for your bot in the Discord Developer Portal. +3. Run this script: python examples/reactions.py +""" + +import asyncio +import os +import sys +import logging +import traceback + +# Add project root to path for local development +if os.path.join(os.getcwd(), "examples") == os.path.dirname(os.path.abspath(__file__)): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +try: + import disagreement + from disagreement.ext import commands + from disagreement.models import Reaction, User, Member +except ImportError: + print( + "Failed to import disagreement. Make sure it's installed or PYTHONPATH is set correctly." + ) + sys.exit(1) + +try: + from dotenv import load_dotenv +except ImportError: # pragma: no cover - example helper + load_dotenv = None + print("python-dotenv is not installed. Environment variables will not be loaded") + +if load_dotenv: + load_dotenv() + +# --- Bot Configuration --- +BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") + +# --- Intents Configuration --- +# We need GUILDS for server context, GUILD_MESSAGES to receive messages, +# and GUILD_MESSAGE_REACTIONS to listen for reaction events. +intents = ( + disagreement.GatewayIntent.GUILDS + | disagreement.GatewayIntent.GUILD_MESSAGES + | disagreement.GatewayIntent.GUILD_MESSAGE_REACTIONS + | disagreement.GatewayIntent.MESSAGE_CONTENT # For commands +) + +# --- Initialize the Client --- +if not BOT_TOKEN: + print("Error: The DISCORD_BOT_TOKEN environment variable is not set.") + sys.exit(1) + +client = disagreement.Client(token=BOT_TOKEN, intents=intents, command_prefix="!") + + +# --- Define a Cog for reaction-related commands --- +class ReactionCog(commands.Cog): + def __init__(self, bot_client): + super().__init__(bot_client) + + @commands.command(name="react") + async def react_command(self, ctx: commands.CommandContext): + """Reacts to the command message with a thumbs up.""" + try: + # The emoji can be a standard Unicode emoji or a custom one in the format '<:name:id>' + await ctx.message.add_reaction("👍") + print(f"Reacted to command from {ctx.author.username}") + except disagreement.HTTPException as e: + print(f"Failed to add reaction: {e}") + await ctx.reply("I couldn't add the reaction. I might be missing permissions.") + + +# --- Event Handlers --- + +@client.event +async def on_ready(): + """Called when the bot is ready and connected to Discord.""" + if client.user: + print(f"Bot is ready! Logged in as {client.user.username}") + else: + print("Bot is ready, but client.user is missing!") + print("------") + print("Reaction example bot is operational.") + + +@client.on_event("MESSAGE_REACTION_ADD") +async def on_reaction_add(reaction: Reaction, user: User | Member): + """Called when a message reaction is added.""" + # We can ignore reactions from the bot itself + if client.user and user.id == client.user.id: + return + + print( + f"Reaction '{reaction.emoji}' added by {user.username} " + f"to message ID {reaction.message_id} in channel ID {reaction.channel_id}" + ) + # You can fetch the message if you need its content, but it's an extra API call. + # try: + # channel = await client.fetch_channel(reaction.channel_id) + # if isinstance(channel, disagreement.TextChannel): + # message = await channel.fetch_message(reaction.message_id) + # print(f" Message content: '{message.content}'") + # except disagreement.errors.NotFound: + # print(" Could not fetch message (maybe it was deleted).") + +@client.on_event("MESSAGE_REACTION_REMOVE") +async def on_reaction_remove(reaction: Reaction, user: User | Member): + """Called when a message reaction is removed.""" + print( + f"Reaction '{reaction.emoji}' removed by {user.username} " + f"from message ID {reaction.message_id} in channel ID {reaction.channel_id}" + ) + + +# --- Main Execution --- +async def main(): + print("Starting Reaction Bot...") + try: + client.add_cog(ReactionCog(client)) + await client.run() + except disagreement.AuthenticationError: + print("Authentication failed. Check your bot token.") + except Exception as e: + print(f"An unexpected error occurred: {e}") + traceback.print_exc() + finally: + if not client.is_closed(): + await client.close() + print("Bot has been shut down.") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file