- Authors
- Name
- Discord Bot Development Setup
- Basic Bot Structure
- Slash Commands
- Button Interactions
- Modal Forms
- Select Menus
- Error Handling
- Moderation Commands
- Deployment
Discord Bot Development Setup
Discord Developer Portal Configuration
- Create a New Application at the Discord Developer Portal
- Copy the Token from the Bot tab (never expose this publicly!)
- Generate a bot invite URL from the OAuth2 tab:
- Scopes:
bot,applications.commands - Permissions: Select the required permissions
- Scopes:
Project Setup
# Create a virtual environment
python -m venv venv
source venv/bin/activate
# Install Pycord
pip install py-cord python-dotenv aiohttp
# Project structure
# my-discord-bot/
# ├── bot.py # Main bot file
# ├── cogs/
# │ ├── __init__.py
# │ ├── general.py # General commands
# │ ├── moderation.py # Moderation commands
# │ └── fun.py # Fun commands
# ├── utils/
# │ └── helpers.py
# ├── .env
# └── requirements.txt
.env File
DISCORD_TOKEN=your_bot_token_here
GUILD_IDS=123456789012345678
Basic Bot Structure
# bot.py
import discord
from discord.ext import commands
import os
from dotenv import load_dotenv
load_dotenv()
# Intents configuration
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
bot = discord.Bot(intents=intents)
@bot.event
async def on_ready():
print(f"✅ {bot.user} logged in!")
print(f"📊 Connected to {len(bot.guilds)} servers")
await bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.watching,
name="Watching the server 👀"
)
)
# Load Cogs
for filename in os.listdir("./cogs"):
if filename.endswith(".py") and not filename.startswith("_"):
bot.load_extension(f"cogs.{filename[:-3]}")
bot.run(os.getenv("DISCORD_TOKEN"))
Slash Commands
# cogs/general.py
import discord
from discord.ext import commands
from discord import option
import aiohttp
from datetime import datetime
class General(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.slash_command(name="ping", description="Check the bot's response time")
async def ping(self, ctx: discord.ApplicationContext):
latency = round(self.bot.latency * 1000)
embed = discord.Embed(
title="🏓 Pong!",
description=f"Latency: **{latency}ms**",
color=discord.Color.green() if latency < 100 else discord.Color.red()
)
await ctx.respond(embed=embed)
@discord.slash_command(name="userinfo", description="Display user information")
@option("user", description="User to view info for", type=discord.Member, required=False)
async def userinfo(self, ctx: discord.ApplicationContext, user: discord.Member = None):
user = user or ctx.author
embed = discord.Embed(
title=f"👤 {user.display_name}",
color=user.color
)
embed.set_thumbnail(url=user.display_avatar.url)
embed.add_field(name="ID", value=user.id, inline=True)
embed.add_field(name="Joined", value=user.joined_at.strftime("%Y-%m-%d"), inline=True)
embed.add_field(name="Account Created", value=user.created_at.strftime("%Y-%m-%d"), inline=True)
embed.add_field(
name="Roles",
value=", ".join([r.mention for r in user.roles[1:]]) or "None",
inline=False
)
await ctx.respond(embed=embed)
@discord.slash_command(name="weather", description="Get weather information")
@option("city", description="City name", type=str, required=True)
async def weather(self, ctx: discord.ApplicationContext, city: str):
await ctx.defer() # Show response delay
async with aiohttp.ClientSession() as session:
url = f"https://wttr.in/{city}?format=j1"
async with session.get(url) as resp:
if resp.status != 200:
await ctx.followup.send("❌ City not found.")
return
data = await resp.json()
current = data["current_condition"][0]
embed = discord.Embed(
title=f"🌤 Weather in {city}",
color=discord.Color.blue()
)
embed.add_field(name="🌡 Temperature", value=f"{current['temp_C']}°C", inline=True)
embed.add_field(name="💧 Humidity", value=f"{current['humidity']}%", inline=True)
embed.add_field(name="💨 Wind", value=f"{current['windspeedKmph']} km/h", inline=True)
embed.add_field(name="Condition", value=current["weatherDesc"][0]["value"], inline=False)
await ctx.followup.send(embed=embed)
def setup(bot):
bot.add_cog(General(bot))
Button Interactions
# cogs/fun.py
import discord
from discord.ext import commands
import random
class RockPaperScissorsView(discord.ui.View):
def __init__(self):
super().__init__(timeout=30)
@discord.ui.button(label="✊ Rock", style=discord.ButtonStyle.primary, custom_id="rock")
async def rock(self, button: discord.ui.Button, interaction: discord.Interaction):
await self.play(interaction, "rock")
@discord.ui.button(label="✋ Paper", style=discord.ButtonStyle.success, custom_id="paper")
async def paper(self, button: discord.ui.Button, interaction: discord.Interaction):
await self.play(interaction, "paper")
@discord.ui.button(label="✌️ Scissors", style=discord.ButtonStyle.danger, custom_id="scissors")
async def scissors(self, button: discord.ui.Button, interaction: discord.Interaction):
await self.play(interaction, "scissors")
async def play(self, interaction: discord.Interaction, user_choice: str):
choices = {"rock": "✊", "paper": "✋", "scissors": "✌️"}
bot_choice = random.choice(list(choices.keys()))
if user_choice == bot_choice:
result = "🤝 Draw!"
color = discord.Color.yellow()
elif (user_choice == "rock" and bot_choice == "scissors") or \
(user_choice == "paper" and bot_choice == "rock") or \
(user_choice == "scissors" and bot_choice == "paper"):
result = "🎉 You Win!"
color = discord.Color.green()
else:
result = "😢 You Lose!"
color = discord.Color.red()
embed = discord.Embed(title=result, color=color)
embed.add_field(name="You", value=choices[user_choice], inline=True)
embed.add_field(name="Bot", value=choices[bot_choice], inline=True)
# Disable buttons
for child in self.children:
child.disabled = True
await interaction.response.edit_message(embed=embed, view=self)
class Fun(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.slash_command(name="rps", description="Rock Paper Scissors game!")
async def rps(self, ctx: discord.ApplicationContext):
embed = discord.Embed(
title="✊✋✌️ Rock Paper Scissors!",
description="Click a button to make your choice!",
color=discord.Color.blue()
)
await ctx.respond(embed=embed, view=RockPaperScissorsView())
def setup(bot):
bot.add_cog(Fun(bot))
Modal Forms
class FeedbackModal(discord.ui.Modal):
def __init__(self):
super().__init__(title="📋 Submit Feedback")
self.add_item(discord.ui.InputText(
label="Title",
placeholder="Enter the feedback title",
style=discord.InputTextStyle.short,
required=True,
max_length=100
))
self.add_item(discord.ui.InputText(
label="Content",
placeholder="Enter detailed content",
style=discord.InputTextStyle.long,
required=True,
max_length=2000
))
self.add_item(discord.ui.InputText(
label="Rating (1-5)",
placeholder="1",
style=discord.InputTextStyle.short,
required=False,
max_length=1
))
async def callback(self, interaction: discord.Interaction):
title = self.children[0].value
content = self.children[1].value
rating = self.children[2].value or "Not provided"
embed = discord.Embed(
title="📋 New Feedback",
color=discord.Color.blue()
)
embed.add_field(name="Title", value=title, inline=False)
embed.add_field(name="Content", value=content, inline=False)
embed.add_field(name="Rating", value=f"{'⭐' * int(rating)}" if rating.isdigit() else rating)
embed.set_footer(text=f"Author: {interaction.user.display_name}")
# Send to feedback channel
feedback_channel = interaction.guild.get_channel(FEEDBACK_CHANNEL_ID)
if feedback_channel:
await feedback_channel.send(embed=embed)
await interaction.response.send_message(
"✅ Feedback submitted! Thank you.", ephemeral=True
)
# Open modal via slash command
@discord.slash_command(name="feedback", description="Submit feedback")
async def feedback(ctx: discord.ApplicationContext):
await ctx.send_modal(FeedbackModal())
Select Menus
class RoleSelectView(discord.ui.View):
@discord.ui.select(
placeholder="Select roles (up to 3)",
min_values=1,
max_values=3,
options=[
discord.SelectOption(label="Developer", emoji="💻", value="developer"),
discord.SelectOption(label="Designer", emoji="🎨", value="designer"),
discord.SelectOption(label="Planner", emoji="📊", value="planner"),
discord.SelectOption(label="Marketer", emoji="📢", value="marketer"),
discord.SelectOption(label="Data Analyst", emoji="📈", value="analyst"),
]
)
async def select_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
selected = ", ".join(select.values)
await interaction.response.send_message(
f"✅ Selected roles: {selected}", ephemeral=True
)
Error Handling
# Add global error handler to bot.py
@bot.event
async def on_application_command_error(ctx: discord.ApplicationContext, error):
if isinstance(error, commands.MissingPermissions):
await ctx.respond("❌ Insufficient permissions.", ephemeral=True)
elif isinstance(error, commands.CommandOnCooldown):
await ctx.respond(
f"⏳ On cooldown. Please try again in {error.retry_after:.1f} seconds.",
ephemeral=True
)
elif isinstance(error, commands.MemberNotFound):
await ctx.respond("❌ User not found.", ephemeral=True)
else:
# Logging
import traceback
traceback.print_exception(type(error), error, error.__traceback__)
await ctx.respond("❌ An error occurred.", ephemeral=True)
Moderation Commands
# cogs/moderation.py
class Moderation(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.slash_command(name="clear", description="Delete messages")
@commands.has_permissions(manage_messages=True)
@option("amount", description="Number of messages to delete", type=int, min_value=1, max_value=100)
async def clear(self, ctx: discord.ApplicationContext, amount: int):
deleted = await ctx.channel.purge(limit=amount)
await ctx.respond(f"🗑️ {len(deleted)} messages deleted", ephemeral=True)
@discord.slash_command(name="slowmode", description="Set slow mode")
@commands.has_permissions(manage_channels=True)
@option("seconds", description="Seconds (0=disable)", type=int, min_value=0, max_value=21600)
async def slowmode(self, ctx: discord.ApplicationContext, seconds: int):
await ctx.channel.edit(slowmode_delay=seconds)
if seconds == 0:
await ctx.respond("✅ Slow mode has been disabled.")
else:
await ctx.respond(f"✅ Slow mode set to {seconds} seconds")
def setup(bot):
bot.add_cog(Moderation(bot))
Deployment
systemd Service
# /etc/systemd/system/discord-bot.service
[Unit]
Description=Discord Bot
After=network.target
[Service]
Type=simple
User=bot
WorkingDirectory=/opt/discord-bot
ExecStart=/opt/discord-bot/venv/bin/python bot.py
Restart=always
RestartSec=10
EnvironmentFile=/opt/discord-bot/.env
[Install]
WantedBy=multi-user.target
sudo systemctl enable discord-bot
sudo systemctl start discord-bot
sudo journalctl -u discord-bot -f
📝 Review Quiz (6 Questions)
Q1. What are Discord Bot Intents?
Intents are settings that specify the types of events the bot will receive. Privileged Intents (message_content, members) require separate activation in the Developer Portal.
Q2. When do you use ctx.defer() in slash commands?
When the response takes more than 3 seconds. After defer(), you send the actual response using ctx.followup.send().
Q3. What does ephemeral=True mean?
It makes the message visible only to the user who executed the command. Other users cannot see it.
Q4. What are the advantages of Cogs?
They allow you to separate commands into modules for better organization and enable dynamic loading/unloading. This is beneficial for code structure and maintainability.
Q5. What does the View's timeout parameter control?
The time in seconds until buttons/select menus are deactivated. Setting it to None means no timeout.
Q6. What is the difference between a Modal and a regular message?
A Modal displays an input form to the user for collecting structured data. Regular messages only exchange text.