- Authors
- Introduction
- Prerequisites
- Why this Gist?
- What are slash commands?
- Installation
- Basic structure for discord.py slash commands
- Foundamentals for this gist
- Basic slash command application using discord.py
- Slash commands within a cog
- An example to use groups with discord.py for slash commands
- Common methods and features used for slash commands
- Add descriptions to args of slash commands with
discord.app_commands.describe - Deferring an interaction with
defer - Checking for Permissions and Roles with
discord.app_commands.checks.has_permissionsdecorator - Adding cooldowns to slash commands with
discord.app_commands.checks.cooldown - Handling errors for slash commands with a global error handler
- Add descriptions to args of slash commands with
- Common issues using slash commands
- Hybrid commands
These are the amazing authors who created this gist!
Upon resumation of the most popular discord API wrapper library for python, discord.py, while catching on to the latest features of the discord API, there have been numerous changes with addition of features to the library.
- Buttons support
- Select Menus Support (AKA Dropdown menus)
- Forms (AKA Modals)
- Slash Commands (AKA Application Commands)
and a bunch of more handy features! All the changes can be found here. Original discord.py gist regarding resumation can be found here.
Make sure that you have fulfilled all of the following.
- Used discord.py before
- Have a basic understanding about a basic bot
- Know how to setup an application or see the discord.py walkthrough.
- Have an ability of reading the documentation
- Have common sense
This Gist is being created as an update to slash commands (app commands) and hybrid commands with explanation and examples.
This Gist mainly focuses on SLASH COMMANDS and HYBRID COMMANDS for discord.py 2.0 (and above)!
Slash Commands are the exciting way to build and interact with bots on Discord. With Slash Commands, all you have to do is type / and you're ready to use your favourite bot.
You can easily see all the commands a bot has, and validation and error handling help you get the command right the first time.
To use slash commands with discord.py the latest up-to-date version has to be installed.
Make sure that the version is 2.0 or above!
And make sure to uninstall any third party libraries that support slash commands for discord.py (if any) as a few of them monkey patch discord.py!
The latest and up-to-date usable discord.py version can be installed using
pip install -U discord.pyIf you don't know what version of discord.py you're using run the following command in the terminal:
pip show discord.pyor add the following line in a python script where discord.py is imported
print(discord.__version__)📌 Note: you could also check the discord.py version doing
python3in the terminal and writing the line above
⚠️ BEFORE MIGRATING TO DISCORD.PY 2.0, PLEASE READ THE CONSEQUENCES OF THE UPDATE HERE.
📌 Note: Slash Commands in discord.py are also referred as Application Commmands and App Commands and every interaction is a webhook.
Slash commands in discord.py are held by a container, CommandTree.
A command tree is required to create slash commands in discord.py. This command tree provides a command method which decorates an asynchronous function indicating to discord.py that the decorated function is intended to be a slash command.
This asynchronous function expects a default argument which acts as the interaction which took place that invoked the slash command.
This default argument is an instance of the Interaction class from discord.py. Further up, the command logic takes over the behaviour of the slash command.
The fundamental for this gist will remain to be the setup_hook. The setup_hook is a special asynchronous method of the Client and Bot class which can be overwritten to perform numerous task.
This method is safe to use as it is always triggered before any events are dispatched, i.e. this method is triggered before the IDENTIFY payload is sent to the discord gateway.
📌 Note: methods of the Bot class such as
change_presencewill not work insetup_hookas the current application does not have an active connection to the gateway at this point.
📋 FOLLOWING IS THE EXAMPLE OF HOW A
SETUP_HOOKFUNCTION CAN BE DEFINED
import discord
'''This is one way of creating a "setup_hook" method'''
class SlashClient(discord.Client):
def __init__(self) -> None:
super().__init__(intents=discord.Intents.default())
async def setup_hook(self) -> None:
#perform tasks
'''Another way of creating a "setup_hook" is as follows'''
client = discord.Client(intents=discord.Intents.default())
async def my_setup_hook() -> None:
#perform tasks
client.setup_hook = my_setup_hook📌 Note: The
CommandTreeclass resides within theapp_commandsof discord.py package (discord.app_commands.CommandTree)
import discord
class SlashClient(discord.Client):
def __init__(self) -> None:
super().__init__(intents=discord.Intents.default())
self.tree = discord.app_commands.CommandTree(self)
async def setup_hook(self) -> None:
self.tree.copy_global_to(guild=discord.Object(id=12345678900987654))
await self.tree.sync()
client = SlashClient()
@client.tree.command(name="ping", description="...")
async def _ping(interaction: discord.Interaction) -> None:
await interaction.response.send_message("pong")
client.run("token")EXPLANATION
import discordimports the discord.py package.class SlashClient(discord.Client)is a class subclassing Client. Though there is no particular reason except readability to subclass the Client class, using theClient.setup_hook = my_funcis equally valid.- Next up
super().__init__(...)runs the__init__function of the Client class, this is equivalent todiscord.Client(...). Then,self.tree = discord.app_commands.CommandTree(self)creates a CommandTree which acts as the container for slash commaands. - Then in the
setup_hook,self.tree.copy_global_to(...)adds the slash command to the guild of which the ID is provided as adiscord.Objectobject. Further up,self.tree.sync()updates the API with any changes to the slash commands. - Finishing up with the Client subclass, we create an instance of the subclassed Client class which here has been named as
SlashClientwithclient = SlashClient(). - Then using the
commandmethod of theCommandTreewe decorate a function with it asclient.treeis an instance ofCommandTreefor the current application. The command function takes a default argument as said, which acts as the interaction that took place. Catching up isawait interaction.response.send_message("pong")which sends back a message to the slash command invoker. - And the classic old
client.run("token")is used to connect the client to the discord gateway. - Note that the
send_messageis a method of theInteractionResponseclass andinteraction.responsein this case is an instance of theInteractionResponseobject. Thesend_messagemethod will not function if the response is not sent within 3 seconds of command invocation. I will discuss about how to handle this issue later following the gist.
import discord
class SlashBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix=".", intents=discord.Intents.default())
async def setup_hook(self) -> None:
self.tree.copy_global_to(guild=discord.Object(id=12345678900987654))
await self.tree.sync()
bot = SlashBot()
@bot.tree.command(name="ping", description="...")
async def _ping(interaction: discord.Interaction) -> None:
await interaction.response.send_message("pong")
bot.run("token")📌 Note: The above example shows a basic slash commands within discord.py using the Bot class.
EXPLANATION
Most of the explanation is the same as the prior example which featured SlashClient which was a subclass of discord.Client. Though some minor changes are discussed below.
- The
SlashBotclass now subclassesdiscord.ext.commands.Botfollowing the passing in of the required arguments to its__init__method. discord.ext.commands.Botalready consists of an instance of theCommandTreeclass which can be accessed using thetreeproperty.
A cog is a collection of commands, listeners, and optional state to help group commands together.
More information on them can be found on the Cogs page.
import discord
from discord.ext import commands
from discord import app_commands
class MySlashCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@app_commands.command(name="ping", description="...")
async def _ping(self, interaction: discord.Interaction):
await interaction.response.send_message("pong!")
class MySlashBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.default())
async def setup_hook(self) -> None:
await self.add_cog(MySlashCog(self))
self.tree.copy_global_to(discord.Object(id=123456789098765432))
await self.tree.sync()
bot = MySlashBot()
bot.run("token")EXPLANATION
- Firstly,
import discordimports the discord.py package.from discord import app_commandsimports theapp_commandsdirectory from the discord.py root directory.from discord.ext import commandsimports the commands extension. - Further up,
class MySlashCog(commands.Cog)is a class subclassing theCogclass. You can read more about this here. def __init__(self, bot: commands.Bot): self.bot = botis the constructor method of the class that is always run when the class is instantiated and that is why we pass in a Bot object whenever we create an instance of the cog class.- Following up is the
@app_commands.command(name="ping", description="...")decorator. This decorator basically functions the same as abot.tree.commandbut since the cog currently does not have a bot, theapp_commands.commanddecorator is used instead. The next two lines follow the same structure for slash commands with self added as the first parameter to the function as it is a method of a class. - The next up lines are mostly the same.
- Talking about the first line inside the
setup_hookis theadd_cogmethod of the Bot class. And since self acts as the instance of the current class, we use self to use theadd_cogmethod of the Bot class as we are inside a subclassed class of the Bot class. Then we pass in self to theadd_cogmethod as the__init__function of the MySlashCog cog accepts aBotobject. - After that we instantiate the
MySlashBotclass and run the bot using the run method which executes our setup_hook function and our commands get loaded and synced. The bot is now ready to use!
📌 Note: That's an example with optional group!
import discord
from discord.ext import commands
from discord import app_commands
class MySlashGroupCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
#--------------------------------------------------------
group = app_commands.Group(name="uwu", description="...")
#--------------------------------------------------------
@app_commands.command(name="ping", description="...")
async def _ping(self, interaction: discord.) -> None:
await interaction.response.send_message("pong!")
@group.command(name="command", description="...")
async def _cmd(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message("uwu")
class MySlashBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.default())
async def setup_hook(self) -> None:
await self.add_cog(MySlashGroupCog(self))
self.tree.copy_global_to(discord.Object(id=123456789098765432))
await self.tree.sync()
bot = MySlashBot()
bot.run("token")EXPLANATION
- The only difference used here is
group = app_commands.Group(name="uwu", description="...")andgroup.command.app_commands.Groupis used to initiate a group whilegroup.commandregisters a command under a group. For example, the ping command can be run using /ping but this is not the case for group commands. They are registered with the format ofgroup_name command_name. So here, the command command of the uwu group would be run using /uwu command.
📌 Note: only group commands can have a single space between them.
import discord
from discord.ext import commands
from discord import app_commands
class MySlashGroup(commands.GroupCog, name="uwu"):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
super().__init__()
@app_commands.command(name="ping", description="...")
async def _ping(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message("pong!")
@app_commands.command(name="command", description="...")
async def _cmd(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message("uwu")
class MySlashBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.default())
async def setup_hook(self) -> None:
await self.add_cog(MySlashGroup(self))
self.tree.copy_global_to(discord.Object(id=123456789098765432))
await self.tree.sync()
bot = MySlashBot()
bot.run("token")EXPLANATION
- The only difference here too is that the
MySlashGroupclass directly subclasses the GroupCog class fromdiscord.ext.commandswhich automatically registers all the methods within the group class to be commands of that specific group. So now, the commands such aspingcan be run using /uwu ping andcommandusing /uwu command.
A common function used for slash commands is the describe function. This is used to add descriptions to the arguments of a slash command. The command function can be decorated with this function. It goes by the following syntax as shown below.
from discord.ext import commands
from discord import app_commands
import discord
bot = commands.Bot(command_prefix=".", intents=discord.Intents.default())
#sync the commands
@bot.tree.command(name="echo", description="...")
@app_commands.describe(text="The text to send!", channel="The channel to send the message in!")
async def _echo(interaction: discord.Interaction, text: str, channel: discord.TextChannel=None):
channel = interaction.channel or channel
await channel.send(text)Another common issue that most people come across is the time duration of sending a message with send_message (a non deferred interaction lasts for 3 seconds). This issue can be tackled by deferring the interaction response using the defer method of the InteractionResponse class. An example for fixing this issue is shown below.
import discord
from discord.ext import commands
import asyncio
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
#sync the commands
@bot.tree.command(name="time", description="...")
async def _time(interaction: discord.Interaction, time_to_wait: int):
# -------------------------------------------------------------
await interaction.response.defer(ephemeral=True, thinking=True)
# -------------------------------------------------------------
await interaction.edit_original_response(content=f"I will notify you after {time_to_wait} seconds have passed!")
await asyncio.sleep(time_to_wait)
await interaction.edit_original_response(content=f"{interaction.user.mention}, {time_to_wait} seconds have already passed!")To add a permissions check to a command, the methods are imported through discord.app_commands.checks. To check for a member's permissions, the function can be decorated with the discord.app_commands.checks.has_permissions method. An example to this as follows.
from discord import app_commands
from discord.ext import commands
import discord
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
#sync commands
@bot.tree.command(name="ping")
@app_commands.checks.has_permissions(manage_messages=True, manage_channels=True) #example permissions
async def _ping(interaction: discord.Interaction):
await interaction.response.send_message("pong!")If the check fails, it will raise a MissingPermissions error which can be handled within an app commands error handler! I will discuss about making an error handler later in the gist. All flags the permissions can be found here.
Other methods that you can decorate the commands with are -
bot_has_permissions| This checks if the bot has the required permissions for executing the slash command. This raises a BotMissingPermissions exception.has_role| This checks if the slash command user has the required role or not. Only ONE role name or role ID can be passed to this. If the name is being passed, make sure to have the exact same name as the role name. This raises a MissingRole exception.- To pass in several role names or role IDs,
has_any_rolecan be used to decorate a command. This raises two exceptions -> MissingAnyRole and NoPrivateMessage
Slash Commands within discord.py can be applied cooldowns to in order to prevent spamming of the commands. This can be done through the discord.app_commands.checks.cooldown method which can be used to decorate a slash command function and register a cooldown to the function. This raises a CommandOnCooldown exception if the command is currently on cooldown.
An example is as follows.
from discord.ext import commands
import discord
class Bot(commands.Bot):
def __init__(self):
super().__init__(command_prefix="uwu", intents=discord.Intents.all())
async def setup_hook(self):
self.tree.copy_global_to(guild=discord.Object(id=12345678909876543))
await self.tree.sync()
bot = Bot()
@bot.tree.command(name="ping")
# -----------------------------------------
@discord.app_commands.checks.cooldown(1, 30)
# -----------------------------------------
async def ping(interaction: discord.Interaction):
await interaction.response.send_message("pong!")
bot.run("token")EXPLANATION
- The first argument the
cooldownmethod takes is the amount of times the command can be run in a specific period of time (in the example 1 time). - The second argument it takes is the period of time in which the command can be run the specified number of times (in the example 30sec).
- The
CommandOnCooldownexception can be handled using an error handler. I will discuss about making an error handler for slash commands later in the gist.
The Slash Commands exceptions can be handled by overwriting the on_error method of the CommandTree. The error handler takes two arguments. The first argument is the Interaction that took place when the error occurred and the second argument is the error that occurred when the slash commands was invoked. The error is an instance of discord.app_commands.AppCommandError which is a subclass of DiscordException.
An example to creating an error handler for slash commands is as follows.
from discord.ext import commands
from discord import app_commands
import discord
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
#sync commands
@bot.tree.command(name="ping")
@app_commands.checks.cooldown(1, 30)
async def ping(interaction: discord.Interaction):
await interaction.response.send_message("pong!")
async def on_tree_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
return await interaction.response.send_message(f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!")
elif isinstance(..., ...):
...
else:
raise error
bot.tree.on_error = on_tree_error
bot.run("token")EXPLANATION
First we create a simple function named as on_tree_error here. To which the first two required arguments are passed, Interaction which is named as interaction here and AppCommandError which is named as error here. Then using simple functions and keywords, we make an error handler like above. Here I have used the isinstance function which takes in an object and a base class as the second argument, this function returns a bool value. After creating the error handler function, we set the function as the error handler for the slash commands. Here, bot.tree.on_error = on_tree_error overwrites the default on_error method of the CommandTree class with our custom error handler which has been named as on_tree_error here.
from discord.ext import commands
from discord import app_commands
import discord
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
#sync commands
@bot.tree.command(name="ping")
app_commands.checks.cooldown(1, 30)
async def ping(interaction: discord.Interaction):
await interaction.response.send_message("pong!")
@ping.error
async def ping_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
return await interaction.response.send_message(f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!")
elif isinstance(..., ...):
...
else:
raise error
bot.run("token")EXPLANATION
Here the command name is simply used to access the error method to decorate a function which acts as the on_error but for a specific command. Please do not call the error method.
discord.errors.Forbidden: 403 Forbidden (error code: 50001): Missing Access. This error is raised when you do not invite the applicaton to your server with the applications.commands scope or your bot has not been granted the necessary permissions to create slash commands. To fix the issue if caused by lack of scopes, following the below steps could get the error fixed!
Go to the application's main page from the discord developer portal. Visit the Url Generator section in Ouath2 and select the applications.commands scope along with the bot scope and then select the permissions your bot requires and create a new invite link. You can re-invite the app to your server using this link. You do not have to kick the app from your server for re-inviting it with the applications.commands scope!
Slash commands not showing up from more than an hour? A day? To fix the issue, you should check if you are actually syncing the tree commands using CommandTree.sync(). If you are already syncing the commands, make sure that you are syncing at the right place too! A sync of the slash commands with the discord API should be performed either within the setup_hook or a prefix command (gives more control). Syncing at the right place also includes that you are to sync the application commands only when your cogs/extensions have been loaded by the Bot (if any). If this does not fix the issue, make sure that if you are specifying any guilds to sync the commands to in particular, the ID of the guild is right. If the commands unfortunately still don't show up, consider checking your slash commands names, descriptions and parameters for any illegal characters.
You should only sync when you perform any of these changes.
- Added or removed a slash command
- Changed a command's name or description
- Modified an argument of a slash command
- Changed an argument's name or description or type
- Modified permissions (
guild_onlydecorator or kwarg |default_permissionsdecorator or kwarg |nsfwkwarg) - Converted a global command to a guild only command or vice versa
A hybrid command is a prefix command and a slash command written under a single function instead of creating separate functions for the same command to be a prefix command as well as a slash command with different decorators. Discord.py got you covered in that case!
Hybrid Commands in discord.py are created using the hybrid_command decorator manually. An asynchronous function is decorated with this function and the function takes a default argument as its first argument. This first argument is handled by discord.py itself and is an instance of a Context object. The behaviour of the command is further taken up by the logic written inside the same asynchronous function! Please note that if your bot does not have the message_content enabled, the prefix commands will not work while the slash commands part continues to work as intended!
import discord
from discord.ext import commands
class HybridBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.all())
async def setup_hook(self) -> None:
self.tree.copy_global_to(guild=discord.Object(id=12345678900987654)) #replace your guild id.
await self.tree.sync()
bot = HybridBot()
@bot.hybrid_command(name="ping")
async def ping(ctx: commands.Context) -> None:
await ctx.defer(ephemeral=True)
await ctx.send(f"My ping is {round(bot.latency * 1000)}ms!", ephemeral=True)
bot.run("token")EXPLANATION
import discordimports the discord.py package.from discord.ext import commandsimports the commands extension from discord.py itself!- Next, a class
HybridBotis set up to subclass commands.Bot with the__init__method of thecommands.botbeing called within theHybridBotclass's__init__function with the required arguments! - Then,
self.tree.copy_global_to(...)indicates to discord.py to include all the tree commands under a guild ID and sync them usingawait self.tree.sync(). bot = HybridBot()creates an instance of the HybridBot class!@bot.hybrid_commandindicates to discord.py that the following function is now a hybrid command!async def ping(ctx: commands.Context)takes a default argumentctxwhich is an instance ofcommands.Contextand is handled by discord.py itself and returns back desired information upon invocation of command!
Please note that if you want to make a global sync, removing the self.tree.copy_global_to(...) will make the sync global!
import discord
from discord.ext import commands
class HybridCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.hybrid_command(name="ping")
async def ping(self, ctx: commands.Context) -> None:
await ctx.defer(ephemeral=True)
await ctx.send(f"My ping is {round(bot.latency * 1000)}ms!", ephemeral=True)
class HybridBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.all())
async def setup_hook(self) -> None:
await self.add_cog(HybridCog(self))
self.tree.copy_global_to(guild=discord.Object(id=12345678900987654)) #replace your guild id.
await self.tree.sync()
bot = HybridBot()
bot.run("token")Please note that if you want to make a global sync, removing the self.tree.copy_global_to(...) will do the same!
import discord
from discord.ext import commands
class HybridBot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.all())
async def setup_hook(self) -> None:
self.tree.copy_global_to(guild=discord.Object(id=12345678900987654)) #replace your guild id.
await self.tree.sync()
bot = HybridBot()
@bot.hybrid_group()
async def main(ctx: commands.Context) -> None:
await ctx.defer()
await ctx.send("pong!")
@main.command()
async def ping(ctx: commands.Context) -> None:
await ctx.defer()
await ctx.send("Pong!")
bot.run("token")📌 Note: if you want to make a global sync, removing the
self.tree.copy_global_to(...)will do the same! Though you can invoke themainhybrid group command with a prefix command such as!mainbut this does not create a slash command with the namemain. A slash command for every subcommand will appear on your servers! An example of how a group command is displayed within discord:main pingfollowing the patterngroup_name subcommandwhich can be invoked here in this case using/main ping.
In the example, re-check the
setup_hook. You have awaited theCommandTree.copy_global_tofunction, which is NOT a coroutine.The same has been done here and here.