Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Makes commonly used classes, functions, and decorators from `disagreement.ext` and `disagreement.ui` submodules directly accessible under the `disagreement` namespace. This change simplifies import statements for users, leading to cleaner and more concise code. Documentation and examples have been updated to reflect these new, simplified import paths.
248 lines
9.0 KiB
Python
248 lines
9.0 KiB
Python
# examples/basic_bot.py
|
|
|
|
"""
|
|
A basic example bot using the Disagreement library.
|
|
|
|
To run this bot:
|
|
1. Make sure you have the 'disagreement' library installed or accessible in your PYTHONPATH.
|
|
If running from the project root, it should be discoverable.
|
|
2. Set the DISCORD_BOT_TOKEN environment variable to your bot's token.
|
|
e.g., export DISCORD_BOT_TOKEN="your_actual_token_here" (Linux/macOS)
|
|
set DISCORD_BOT_TOKEN="your_actual_token_here" (Windows CMD)
|
|
$env:DISCORD_BOT_TOKEN="your_actual_token_here" (Windows PowerShell)
|
|
3. Run this script: python examples/basic_bot.py
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import logging # Optional: for more detailed logging
|
|
|
|
# Assuming the 'disagreement' package is in the parent directory or installed
|
|
import sys
|
|
import traceback
|
|
|
|
# Add project root to path if running script directly from examples folder
|
|
# and disagreement is not installed
|
|
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:
|
|
from disagreement import (
|
|
Client,
|
|
GatewayIntent,
|
|
Message,
|
|
Guild,
|
|
AuthenticationError,
|
|
DisagreementException,
|
|
Cog,
|
|
command,
|
|
CommandContext,
|
|
)
|
|
except ImportError:
|
|
print(
|
|
"Failed to import disagreement. Make sure it's installed or PYTHONPATH is set correctly."
|
|
)
|
|
print(
|
|
"If running from the 'examples' directory, try running from the project root: python -m examples.basic_bot"
|
|
)
|
|
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()
|
|
|
|
# Optional: Configure logging for more insight, especially for gateway events
|
|
# logging.basicConfig(level=logging.DEBUG) # For very verbose output
|
|
# logging.getLogger('disagreement.gateway').setLevel(logging.INFO) # Or DEBUG
|
|
# logging.getLogger('disagreement.http').setLevel(logging.INFO)
|
|
|
|
# --- Bot Configuration ---
|
|
BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN")
|
|
|
|
# --- Intents Configuration ---
|
|
# Define the intents your bot needs. For basic message reading and responding:
|
|
intents = (
|
|
GatewayIntent.GUILDS
|
|
| GatewayIntent.GUILD_MESSAGES
|
|
| GatewayIntent.MESSAGE_CONTENT
|
|
) # MESSAGE_CONTENT is privileged!
|
|
|
|
# If you don't need message content and only react to commands/mentions,
|
|
# you might not need MESSAGE_CONTENT intent.
|
|
# intents = GatewayIntent.default() # A good starting point without privileged intents
|
|
# intents |= GatewayIntent.MESSAGE_CONTENT # Add if needed
|
|
|
|
# --- Initialize the Client ---
|
|
if not BOT_TOKEN:
|
|
print("Error: The DISCORD_BOT_TOKEN environment variable is not set.")
|
|
print("Please set it before running the bot.")
|
|
sys.exit(1)
|
|
|
|
# Initialize Client with a command prefix
|
|
client = Client(token=BOT_TOKEN, intents=intents, command_prefix="!")
|
|
|
|
|
|
# --- Define a Cog for example commands ---
|
|
class ExampleCog(Cog): # Ensuring this uses commands.Cog
|
|
def __init__(
|
|
self, bot_client
|
|
): # Renamed client to bot_client to avoid conflict with self.client
|
|
super().__init__(bot_client) # Pass the client instance to the base Cog
|
|
|
|
@command(name="hello", aliases=["hi"])
|
|
async def hello_command(self, ctx: CommandContext, *, who: str = "world"):
|
|
"""Greets someone."""
|
|
await ctx.reply(f"Hello {ctx.author.mention} and {who}!")
|
|
print(f"Executed 'hello' command for {ctx.author.username}, greeting {who}")
|
|
|
|
@command()
|
|
async def ping(self, ctx: CommandContext):
|
|
"""Responds with Pong!"""
|
|
await ctx.reply("Pong!")
|
|
print(f"Executed 'ping' command for {ctx.author.username}")
|
|
|
|
@command()
|
|
async def me(self, ctx: CommandContext):
|
|
"""Shows information about the invoking user."""
|
|
reply_content = (
|
|
f"Hello {ctx.author.mention}!\n"
|
|
f"Your User ID is: {ctx.author.id}\n"
|
|
f"Your Username: {ctx.author.username}#{ctx.author.discriminator}\n"
|
|
f"Are you a bot? {'Yes' if ctx.author.bot else 'No'}"
|
|
)
|
|
await ctx.reply(reply_content)
|
|
print(f"Executed 'me' command for {ctx.author.username}")
|
|
|
|
@command(name="add")
|
|
async def add_numbers(self, ctx: CommandContext, num1: int, num2: int):
|
|
"""Adds two numbers."""
|
|
result = num1 + num2
|
|
await ctx.reply(f"The sum of {num1} and {num2} is {result}.")
|
|
print(
|
|
f"Executed 'add' command for {ctx.author.username}: {num1} + {num2} = {result}"
|
|
)
|
|
|
|
@command(name="say")
|
|
async def say_something(self, ctx: CommandContext, *, text_to_say: str):
|
|
"""Repeats the text you provide."""
|
|
await ctx.reply(f"You said: {text_to_say}")
|
|
print(
|
|
f"Executed 'say' command for {ctx.author.username}, saying: {text_to_say}"
|
|
)
|
|
|
|
@command(name="whois")
|
|
async def whois(self, ctx: CommandContext, *, name: str):
|
|
"""Looks up a member by username or nickname using the guild cache."""
|
|
if not ctx.guild:
|
|
await ctx.reply("This command can only be used in a guild.")
|
|
return
|
|
|
|
member = ctx.guild.get_member_named(name)
|
|
if member:
|
|
await ctx.reply(
|
|
f"Found: {member.username}#{member.discriminator} (display: {member.display_name})"
|
|
)
|
|
else:
|
|
await ctx.reply("Member not found in cache.")
|
|
|
|
@command(name="quit")
|
|
async def quit_command(self, ctx: CommandContext):
|
|
"""Shuts down the bot (requires YOUR_USER_ID to be set)."""
|
|
# Replace YOUR_USER_ID with your actual Discord User ID for a safe shutdown command
|
|
your_user_id = "YOUR_USER_ID_REPLACE_ME" # IMPORTANT: Replace this
|
|
if str(ctx.author.id) == your_user_id:
|
|
print("Quit command received. Shutting down...")
|
|
await ctx.reply("Shutting down...")
|
|
await self.client.close() # Access client via self.client from Cog
|
|
else:
|
|
await ctx.reply("You are not authorized to use this command.")
|
|
print(
|
|
f"Unauthorized quit attempt by {ctx.author.username} ({ctx.author.id})"
|
|
)
|
|
|
|
|
|
# --- 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}#{client.user.discriminator}"
|
|
)
|
|
print(f"User ID: {client.user.id}")
|
|
else:
|
|
print("Bot is ready, but client.user is missing!")
|
|
print("------")
|
|
print("Disagreement Bot is operational.")
|
|
print("Listening for commands...")
|
|
|
|
|
|
@client.event
|
|
async def on_message(message: Message):
|
|
"""Called when a message is created and received."""
|
|
# Command processing is now handled by the CommandHandler via client._process_message_for_commands
|
|
# This on_message can be used for other message-related logic if needed,
|
|
# or removed if all message handling is command-based.
|
|
|
|
# Example: Log all messages (excluding bot's own, if client.user was available)
|
|
# if client.user and message.author.id == client.user.id:
|
|
# return
|
|
|
|
print(
|
|
f"General on_message: #{message.channel_id} from {message.author.username}: {message.content}"
|
|
)
|
|
# The old if/elif command structure is no longer needed here.
|
|
|
|
|
|
@client.on_event(
|
|
"GUILD_CREATE"
|
|
) # Example of listening to a specific event by its Discord name
|
|
async def on_guild_available(guild: Guild):
|
|
# The event now passes a Guild object directly
|
|
print(f"Guild available: {guild.name} (ID: {guild.id})")
|
|
|
|
|
|
# --- Main Execution ---
|
|
async def main():
|
|
print("Starting Disagreement Bot...")
|
|
try:
|
|
# Add the Cog to the client
|
|
client.add_cog(ExampleCog(client)) # Pass client instance to Cog constructor
|
|
# client.add_cog is synchronous, but it schedules cog.cog_load() if it's async.
|
|
|
|
await client.run()
|
|
except AuthenticationError:
|
|
print(
|
|
"Authentication failed. Please check your bot token and ensure it's correct."
|
|
)
|
|
except DisagreementException as e:
|
|
print(f"A Disagreement library error occurred: {e}")
|
|
except KeyboardInterrupt:
|
|
print("Bot shutting down due to KeyboardInterrupt...")
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred: {e}")
|
|
traceback.print_exc()
|
|
finally:
|
|
if not client.is_closed():
|
|
print("Ensuring client is closed...")
|
|
await client.close()
|
|
print("Bot has been shut down.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Note: On Windows, the default asyncio event loop policy might not support add_signal_handler.
|
|
# If you encounter issues with Ctrl+C not working as expected,
|
|
# you might need to adjust the event loop policy or handle shutdown differently.
|
|
# For example, for Windows:
|
|
# if os.name == 'nt':
|
|
# asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
|
|
asyncio.run(main())
|