Welcome! This guide will help you understand Discord bot development from the ground up using the BotForge25 project.
- Understanding Discord Bots
- Project Setup Explained
- Code Breakdown
- Command Types Explained
- Discord Concepts
- Step-by-Step Tutorial
- Common Patterns
- Next Steps
A Discord bot is an automated program that can:
- Respond to commands
- Send messages
- Manage servers
- Moderate content
- Play music
- And much more!
- Bot Account: You create a special bot account through Discord Developer Portal
- Token: Discord gives you a secret token (like a password)
- Code: Your Python code uses this token to connect to Discord
- Events: The bot listens for events (messages, commands, etc.)
- Responses: The bot reacts to events based on your code
python -m venv venvWhat it does: Creates an isolated Python environment so packages don't conflict with other projects.
Why it matters: Different projects might need different versions of libraries.
Windows:
venv\Scripts\activatemacOS/Linux:
source venv/bin/activateWhat it does: Switches your terminal to use the isolated environment.
pip install -r requirements.txtWhat it does: Installs all required Python packages listed in requirements.txt.
BOT_TOKEN=your_bot_token_here
Why use .env: Keeps secrets separate from code so you don't accidentally share them.
Let's break down main.py section by section:
import discord
from discord.ext import commandsExplanation:
discord: The main discord.py librarycommands: Extension that adds command functionality
my_intents = discord.Intents.default()
my_intents.message_content = TrueWhat are Intents? Intents tell Discord what events your bot wants to receive. Think of them as permissions for what the bot can "see."
Common Intents:
message_content: See message textmembers: Access member informationguilds: Access server information
Important: You must enable these in the Discord Developer Portal too!
from dotenv import load_dotenv
import os
load_dotenv()
TOKEN = os.getenv("BOT_TOKEN")Explanation:
load_dotenv(): Reads the.envfileos.getenv("BOT_TOKEN"): Gets the bot token from environment variables
my_bot = commands.Bot(
command_prefix="?",
intents=my_intents,
)Explanation:
command_prefix="?": Prefix commands start with?intents=my_intents: Passes the intents we configured
@my_bot.event
async def on_ready():
await my_bot.tree.sync()
print(f"Logged in as {my_bot.user}")Explanation:
- Triggered when bot successfully connects to Discord
tree.sync(): Registers slash commands with Discord- Prints confirmation message
if __name__ == "__main__":
my_bot.run(TOKEN)Explanation:
- Starts the bot using your token
- Connects to Discord and keeps the bot online
@my_bot.command(name="helloworld")
async def hello_world(ctx):
await ctx.send("Hello, World!")Usage: ?helloworld
How it works:
- User types
?helloworld - Bot detects the prefix
? - Matches command name
helloworld - Runs the function
- Sends response
Key Points:
ctx(context): Contains information about who sent the command, where, etc.ctx.send(): Sends a message to the same channel
@my_bot.tree.command(name="byeworld")
async def byeworld(interaction: discord.Interaction):
await interaction.response.send_message("Bye World!")Usage: /byeworld
How it works:
- User types
/and selects command from menu - Discord sends interaction to bot
- Bot responds using
interaction.response
Key Differences:
- Uses
interactioninstead ofctx - Must use
interaction.response.send_message() - Appears in Discord's command menu
@my_bot.hybrid_command(name="greet")
async def greet(ctx):
user_id = ctx.author.id
await ctx.send(f"<@{user_id}> Greetings!")Usage: ?greet OR /greet
Why use hybrid?
- Works both ways!
- Users can choose their preferred method
- Best of both worlds
The ctx parameter contains everything about the command:
ctx.author # The user who sent the command
ctx.guild # The server (guild) where command was sent
ctx.channel # The channel where command was sent
ctx.message # The actual message (for prefix commands)For slash commands, interaction provides similar information:
interaction.user # The user who used the command
interaction.guild # The server
interaction.channel # The channelIn Discord API, servers are called "guilds":
ctx.guild.name # Server name
ctx.guild.id # Server ID
ctx.guild.member_count # Number of members
ctx.guild.owner # Server owner# Get all channels
all_channels = await ctx.guild.fetch_channels()
# Text channels only
text_channels = ctx.guild.text_channels
# Voice channels only
voice_channels = ctx.guild.voice_channels
# Get specific channel by ID
channel = ctx.guild.get_channel(channel_id)- Text Channels: For text messages
- Voice Channels: For voice chat
- Category Channels: Organize other channels
- Stage Channels: For large audio events
- Forum Channels: Thread-based discussions
Let's create a command that responds with current time:
from datetime import datetime
@my_bot.hybrid_command(name="time")
async def current_time(ctx):
now = datetime.now()
time_string = now.strftime("%H:%M:%S")
await ctx.send(f"Current time: {time_string}")Breakdown:
- Import datetime module
- Create hybrid command named "time"
- Get current time
- Format it as string
- Send response
@my_bot.hybrid_command(name="say")
async def say_command(ctx, *, message: str):
await ctx.send(message)Breakdown:
*, message: str: Takes all text after command as one parameter*means "consume remaining arguments"- Usage:
?say Hello everyone!→ Bot says "Hello everyone!"
@my_bot.hybrid_command(name="safesend")
async def safesend(ctx, channel: discord.TextChannel, *, message: str):
if channel.id == ctx.channel.id:
await ctx.send("NOT ALLOWED")
else:
await channel.send(message)
await ctx.send(f"Message sent to {channel.name}!")Breakdown:
- Takes channel and message as parameters
- Checks if target channel is same as current channel
- If same, denies request
- If different, sends message to target channel
- Confirms action
# Method 1: Using mention property
await ctx.send(ctx.author.mention)
# Method 2: Using ID with formatting
user_id = ctx.author.id
await ctx.send(f"<@{user_id}>")Result: @Username (clickable mention)
# Multi-line message
message = (
f"Name: {name}\n"
f"Age: {age}\n"
f"Role: {role}"
)
await ctx.send(message)
# Using f-strings
await ctx.send(f"Welcome {user.name} to {guild.name}!")@my_bot.hybrid_command(name="kick")
async def kick_user(ctx, member: discord.Member):
try:
await member.kick()
await ctx.send(f"{member.name} was kicked!")
except discord.Forbidden:
await ctx.send("I don't have permission to kick members!")
except Exception as e:
await ctx.send(f"An error occurred: {e}")@my_bot.hybrid_command(name="listmembers")
async def list_members(ctx):
members = ctx.guild.members
member_list = "\n".join([m.name for m in members])
await ctx.send(f"Members:\n{member_list}")Answer:
async: Marks a function as asynchronous (can wait without blocking)await: Waits for something to complete before continuing- Discord bots are asynchronous because they handle multiple things at once
Answer:
It's just a naming convention. You could name it anything, but ctx is standard in discord.py community.
await ctx.send("Message") # Sends normal message
await ctx.reply("Message") # Sends message that references original@my_bot.hybrid_command(name="admin")
async def admin_command(ctx):
allowed_channel_id = 123456789 # Replace with your channel ID
if ctx.channel.id != allowed_channel_id:
await ctx.send("This command only works in the admin channel!")
return
# Your command logic here
await ctx.send("Admin command executed!")@my_bot.hybrid_command(name="clear")
@commands.has_permissions(manage_messages=True)
async def clear_messages(ctx, amount: int):
await ctx.channel.purge(limit=amount)Solutions:
- Check if bot is running (
python main.py) - Verify token is correct
- Check internet connection
Solutions:
- Check if you used correct prefix (
?) - For slash commands, wait for sync (can take time)
- Check bot has permission to send messages
Solutions:
- Check bot role permissions in server settings
- Ensure bot role is higher than target roles
- Verify intents are enabled in Developer Portal
Solution: Enable "Message Content Intent" in:
- Discord Developer Portal → Bot → Privileged Gateway Intents
- Your code:
my_intents.message_content = True
@my_bot.hybrid_command(name="divide")
async def divide(ctx, a: int, b: int):
try:
result = a / b
await ctx.send(f"Result: {result}")
except ZeroDivisionError:
await ctx.send("Cannot divide by zero!")@my_bot.hybrid_command(name="ban")
@commands.has_permissions(ban_members=True)
async def ban_user(ctx, member: discord.Member):
await member.ban()
await ctx.send(f"{member.name} was banned!")@ban_user.error
async def ban_error(ctx, error):
if isinstance(error, commands.MissingPermissions):
await ctx.send("You don't have permission to ban members!")async def greet(ctx, name: str, age: int):
# Discord auto-validates types
await ctx.send(f"Hello {name}, age {age}!")- Dice Roller: Random number generator for games
- Poll Bot: Create polls with reactions
- Reminder Bot: Send reminders after time delay
- Weather Bot: Fetch weather from API
- Quote Bot: Random inspirational quotes
- Cogs (organizing commands into modules)
- Database integration (SQLite, MongoDB)
- Embeds (fancy formatted messages)
- Buttons and Select Menus
- Modal forms for user input
- Voice channel interaction
- Auto-moderation
- Economy systems
- Music playback
- Web dashboard
Create a command that takes two numbers and an operation (+, -, *, /) and returns the result.
Create a command that shows how many members have specific roles.
Make the bot send a welcome message when a new member joins the server.
Allow server admins to set a custom prefix for your bot per server.
Let users get roles by clicking reaction emojis.
Congratulations! You now have a solid foundation in Discord bot development. Remember:
- Start small: Don't try to build everything at once
- Read errors: Error messages tell you what's wrong
- Test often: Test each feature as you add it
- Ask for help: The Discord.py community is helpful
- Have fun: Bot development is creative and rewarding!
Happy coding! 🚀
Last updated: December 2025