Table of contents
- Why Slash Commands?
- Creating the application directory - MyBot
- Setting up the virtual environment
- Why are we setting up a virtual environment?
- Discord python library - discord.py
- Cogs & Extensions
- Sync
- Why should we not auto-sync?
- When to Sync?
- When not to Sync?
- How to sync without restarting the bot?
- Reloading Extensions
- How to load/unload/reload the extensions without restarting the bot?
- Creating Simple Slash Commands
- say_hi command
- add_numbers command
- Final Code
In this series, I'll explain the best ways to code your Discord bot in Python. We will be creating Slash Commands. I've learned these tips from making my own bots over the years.
Before we start, make sure you've set up your bot from the Developer Console and added it to your test server. If you need help with this, you can find lots of guides online.
Don't forget to check on Slash Commands while inviting your bot to the server!
I will be explaining giving code snippets in between while explaining them, and the final code will be given at the end.
Why Slash Commands?
Discord has designated message content as a Privileged Intent for privacy and performance reasons. To access message content for your bot, you must obtain permission if it is on 100+ servers.
Prefix commands require access to message content to function. Therefore, approval for the Privileged Intent will only be granted if your bot is specifically designed to read, moderate, and manage Discord user messages. Using slash commands is a better option if this is not the case.
For more details, check out:
1. MESSAGE CONTENT IS BECOMING A NEW PRIVILEGED INTENT
2. WHY WE MOVED TO SLASH COMMANDS
Creating the application directory - MyBot
Create your application directory, /MyBot
. This directory will contain your main python file - main.py
. Create 3 folders - cogs
, tokens
, and utils
.
/cogs
will contain the python files with all your commands.
/tokens
will contain .env files that will hold your secret tokens of discord and any other API that you use.
/utils
will contain any other helper files like python or text files.
Setting up the virtual environment
If you are using VSCode, go to the Terminal
cd MyBot
python -m venv env
-> create a new virtual env called ‘env’.\env\Scripts\activate
-> activate the virtual environment
This will create a new directory - env
in MyBot
.
Why are we setting up a virtual environment?
This will make sure that any module you install for your bot will not clash with the modules of other applications. And when we create a requirements.txt file, the file will contain only the dependencies that are required for your bot.
Discord python library - discord.py
discord.py is a Python library for the Discord API, allowing easy access to the API functions and handling of events. It is used to create Discord bots and interact with the Discord API to perform tasks such as sending messages, managing servers, and more.
Terminal
-> pip install discord.py
Cogs & Extensions
Cogs are classes that derive from commands.
Cogs are usually placed in separate files and loaded as extensions.
The combination of cogs and extensions allows you to separate commands, events, tasks, and other things into separate files and classes for better organization. They help you to write clean, maintainable code and make it easier to add new features and functionality to your bot over time. They are highly recommended.
We will be creating 2 cogs - OwnerCommands
and UserCommands
, in 2 separate files - OwnerCommands.py
and UserCommands.py
under the directory, /cogs
, and load them in main.py
.
main.py
def __init__(self):
self.initial_extensions = [
"cogs.OwnerCommands",
"cogs.UserCommands"
]
async def setup_hook(self):
for ext in self.initial_extensions:
await self.load_extension(ext)
NOTE: To avoid restarting the bot every time we make any changes to the bot**,** we will write owner commands that will sync and reload the commands directly from the Discord server.
Sync
When someone uses a slash command, Discord tells your bot about it. And for this, you need to add your commands to Discord's list of commands. In order to do this, you need to register your commands on the command tree and then tell discord they exist by syncing with tree.sync
.
Why should we not auto-sync?
Syncing your command tree automatically causes extra unnecessary requests to be made, which is pointless and a waste of an API request to a limit with an already tight rate limit.
When to Sync?
Added/removed a command
Changed a command's
name
description
Added/removed an argument
Changed an argument's
name (rename decorator)
description (describe decorator)
type (arg:
str
str is the type here)
Added/modified permissions:
guild_only decorator or kwarg
default_permissions decorator or kwarg
NSFW kwarg
Added/modified/removed locale strings
Converted the global/guild command to a guild/global command
When not to Sync?
Changed anything in the function's body (after the
async def ():
part)Added or modified a library side check:
@app_commands.checks
@commands...(.check)
@app_commands.checks.(dynamic_)cooldown(...)
How to sync without restarting the bot?
OwnerCommands.py
@commands.command()
@commands.guild_only()
@commands.is_owner()
async def sync(self, ctx: commands.Context, guilds: commands.Greedy[discord.Object], spec: Optional[Literal["~", "*", "^"]] = None) -> None:
if not guilds:
if spec == "~":
synced = await ctx.bot.tree.sync(guild=ctx.guild)
elif spec == "*":
ctx.bot.tree.copy_global_to(guild=ctx.guild)
synced = await ctx.bot.tree.sync(guild=ctx.guild)
elif spec == "^":
ctx.bot.tree.clear_commands(guild=ctx.guild)
await ctx.bot.tree.sync(guild=ctx.guild)
synced = []
else:
synced = await ctx.bot.tree.sync()
await ctx.send(
f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}"
)
return
ret = 0
for guild in guilds:
try:
await ctx.bot.tree.sync(guild=guild)
except discord.HTTPException:
pass
else:
ret += 1
await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.")
With the above code, you can tag the bot in your server and sync the commands as given below
@MyBot sync
-> global sync
@MyBot sync ~
-> sync current guild
@MyBot sync *
-> copies all global app commands to the current guild and syncs
@MyBot sync ^
-> clears all commands from the current guild target and syncs (removes guild commands)
@MyBot sync id_1 id_2
-> syncs guilds with id 1 and 2
Reloading Extensions
You need to reload extensions whenever you change anything in the code and want to see the new changes in your bot, i.e., whenever you make any changes to the body of your command.
How to load/unload/reload the extensions without restarting the bot?
OwnerCommands.py
#Command which Loads a Module. Remember to use dot path. e.g: cogs.owner
@commands.command(name='load')
@commands.is_owner()
async def load_cog(self, ctx: commands.Context, *, cog: str):
try:
await self.bot.load_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
else:
await ctx.send('**`SUCCESS`**')
#Command which Unloads a Module. Remember to use dot path. e.g: cogs.owner
@commands.command(name='unload')
@commands.is_owner()
async def unload_cog(self, ctx: commands.Context, *, cog: str):
try:
await self.bot.unload_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
else:
await ctx.send('**`SUCCESS`**')
#Command which Reloads a Module. Remember to use dot path. e.g: cogs.owner
@commands.command(name='reload')
@commands.is_owner()
async def reload_cog(self, ctx: commands.Context, *, cog: str):
try:
await self.bot.unload_extension(cog)
await self.bot.load_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
else:
await ctx.send('**`SUCCESS`**')
With the above code, you can tag the bot in your server and reload the UserCommands
extension as @MyBot reload cogs.UserCommands
Creating Simple Slash Commands
say_hi
command
UserCommands.py
# say_hi command
@app_commands.command(name='say_hi', description="Says Hi to you")
async def say_hi(self, interaction: discord.Interaction, name: str):
reply = f"Hi {name}!"
await interaction.response.send_message(reply)
In say_hi
command, name
is the input we get from the user.
add_numbers
command
UserCommands.py
# add_numbers command
@app_commands.command(name='add_numbers', description="Adds 2 numbers")
async def add_numbers(self, interaction: discord.Interaction, num1: int, num2: int):
reply = num1+num2
await interaction.response.send_message(reply)
In add_numbers
command, num1
and num2
are the inputs we get from the user.
Final Code
Terminal
-> pip install python-dotenv
main.py
import os
import discord
from discord.ext import commands
#import logging
# Get discord token from the file
from dotenv import load_dotenv
load_dotenv('tokens/discord_token.env')
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
#handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
class MyBot(commands.Bot):
def __init__(self):
intents = discord.Intents().default()
super().__init__(
command_prefix=commands.when_mentioned,
intents=intents,
status=discord.Status.online,
activity=discord.Game("/help"),
)
self.initial_extensions = [
"cogs.OwnerCommands",
"cogs.UserCommands"
]
async def setup_hook(self):
for ext in self.initial_extensions:
await self.load_extension(ext)
async def close(self):
await super().close()
await self.session.close()
async def on_ready(self):
print(f'{self.user} has connected to Discord')
bot = MyBot()
bot.run(DISCORD_TOKEN)
#bot.run(DISCORD_TOKEN, log_handler=handler, log_level=logging.DEBUG)
Here, command_prefix=commands.when_mentioned
is given so that we can tag the bot for sync and reloading extensions.
UserCommands.py
import discord
from discord import app_commands
from discord.ext import commands
class UserCommands(commands.Cog):
def __init__(self, bot):
self.bot = bot
# say_hi command
@app_commands.command(name='say_hi', description="Says Hi to you")
async def say_hi(self, interaction: discord.Interaction, name: str):
reply = f"Hi {name}!"
await interaction.response.send_message(reply)
# add_numbers command
@app_commands.command(name='add_numbers', description="Adds 2 numbers")
async def add_numbers(self, interaction: discord.Interaction, num1: int, num2: int):
reply = num1+num2
await interaction.response.send_message(reply)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(UserCommands(bot))
We are making all the User Commands global
OwnerCommands.py
import discord
from discord import app_commands
from discord.ext import commands
from typing import Optional, Literal
class OwnerCommands(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.guild_only()
@commands.is_owner()
async def sync(self, ctx: commands.Context, guilds: commands.Greedy[discord.Object], spec: Optional[Literal["~", "*", "^"]] = None) -> None:
if not guilds:
if spec == "~":
synced = await ctx.bot.tree.sync(guild=ctx.guild)
elif spec == "*":
ctx.bot.tree.copy_global_to(guild=ctx.guild)
synced = await ctx.bot.tree.sync(guild=ctx.guild)
elif spec == "^":
ctx.bot.tree.clear_commands(guild=ctx.guild)
await ctx.bot.tree.sync(guild=ctx.guild)
synced = []
else:
synced = await ctx.bot.tree.sync()
await ctx.send(
f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}"
)
return
ret = 0
for guild in guilds:
try:
await ctx.bot.tree.sync(guild=guild)
except discord.HTTPException:
pass
else:
ret += 1
await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.")
#Command which Loads a Module. Remember to use dot path. e.g: cogs.owner
@commands.command(name='load')
@commands.is_owner()
async def load_cog(self, ctx: commands.Context, *, cog: str):
try:
await self.bot.load_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
else:
await ctx.send('**`SUCCESS`**')
#Command which Unloads a Module. Remember to use dot path. e.g: cogs.owner
@commands.command(name='unload')
@commands.is_owner()
async def unload_cog(self, ctx: commands.Context, *, cog: str):
try:
await self.bot.unload_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
else:
await ctx.send('**`SUCCESS`**')
#Command which Reloads a Module. Remember to use dot path. e.g: cogs.owner
@commands.command(name='reload')
@commands.is_owner()
async def reload_cog(self, ctx: commands.Context, *, cog: str):
try:
await self.bot.unload_extension(cog)
await self.bot.load_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
else:
await ctx.send('**`SUCCESS`**')
# sync the OwnerCommands only in the BotTest Server
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(
OwnerCommands(bot),
guild = discord.Object(id=111111111111111111)) #give your Server ID here
We are making all the Owner Commands guild-only, i.e., these commands will be available only in a particular guild (give your own Server ID)
discord_token.env
DISCORD_TOKEN=ABCD1234
Give your Discord bot's Secret Token here
I hope you found this blog helpful!
I will be writing more blogs on how to add more features to your Discord Bot.
Happy Coding!! :D