246 lines
8.9 KiB
Python
246 lines
8.9 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 ---
|
|
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.
|
|
|
|
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...")
|
|
asyncio.run(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())
|
|
|
|
main()
|