This guide will walkthrough the ways to create a custom help command by subclassing HelpCommand.
In simple terms, a subclass is a way to inherit a class behaviour/attributes from another class. Here's how you would subclass a class in Python.
class A:
def __init__(self, attribute1):
self.attribute1 = attribute1
def method1(self):
print("method 1")
def method2(self):
print("method 2")
class B(A):
def __init__(self, attribute1, attribute2):
super().__init__(attribute1) # This calls A().__init__ magic method.
self.attribute2 = attribute2
def method1(self): # Overrides A().method1
print("Hi")
Given the example, the variable instance_a
contains the instance of class A
. As expected, the output will be this.
>>> instance_a = A(1)
>>> instance_a.attribute1
1
>>> instance_a.method1()
"method 1"
How about B
class? instance_b
contains the instance of class B
, it inherits attributes/methods from class A
. Meaning
it will have everything that class A
have, but with an additional attribute/methods.
>>> instance_b = B(1, 2)
>>> instance_b.attribute1
1
>>> instance_b.attribute2
2
>>> instance_b.method1()
"Hi"
>>> instance_b.method2()
"method 2"
Make sure to look and practice more into subclassing classes to understand fully on what it is before diving into subclassing HelpCommand.
Firstly, let me show you the wrong way of creating a help command.
bot = commands.Bot(command_prefix="uwu ", help_command=None)
# OR
bot.help_command = None
# OR
bot.remove_command("help")
@bot.command()
async def help(ctx):
...
This is directly from YouTube, which are known to have bad tutorials for discord.py.
Missing out on command handling that are specifically for HelpCommand.
For instance, say my prefix is !
. Command handling such as
!help
!help <command>
!help <group>
!help <cog>
For a HelpCommand, all of these are handled in the background, including showing
appropriate error when command
/group
/cog
are an invalid argument that were
given. You can also show custom error when an invalid argument are given.
For people who remove the HelpCommand? There is no handling, you have to do it yourself.
For example
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="!", help_command=None, intents=intents)
@bot.command()
async def help(ctx, argument=None):
# !help
if argument is None:
await ctx.send("This is help")
elif argument in bot.all_commands:
command = bot.get_command(argument)
if isinstance(command, commands.Group):
# !help <group>
await ctx.send("This is help group")
else:
# !help <command>
await ctx.send("This is help command")
elif argument in bot.cogs:
# !help <cog>
cog = bot.get_cog(argument)
await ctx.send("This is help cog")
else:
await ctx.send("Invalid command or cog")
This is an ok implementation and all, but you have to handle more than this. I'm only simplifying the code.
Now for the subclassed HelpCommand code.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="!", intents=intents)
class MyHelp(commands.HelpCommand):
# !help
async def send_bot_help(self, mapping):
await self.context.send("This is help")
# !help <command>
async def send_command_help(self, command):
await self.context.send("This is help command")
# !help <group>
async def send_group_help(self, group):
await self.context.send("This is help group")
# !help <cog>
async def send_cog_help(self, cog):
await self.context.send("This is help cog")
bot.help_command = MyHelp()
Not only does HelpCommand looks better, it is also much more readable compared to the bad way
.
Oh, did I mention that HelpCommand also handles invalid arguments for you? Yeah. It does.
HelpCommand contains a bunch of useful methods you can use in order to assist you in creating your help command and formatting.
Methods / Attributes | Usage |
---|---|
HelpCommand.filter_commands() | Filter commands to only show commands that the user can run. This help hide any secret commands from the general user. |
Context.clean_prefix | HelpCommand.clean_prefix was removed in version 2.0 of discord.py and replaced with Context.clean_prefix . This cleans your prefix from @everyone and @here as well as @mentions |
HelpCommand.get_command_signature() | Get the command signature and format them such as command [argument] for optional and command <argument> for required. |
HelpCommand.prepare_help_command() | Triggers before every send_x_help method are triggered, this work exactly like command.before_invoke |
HelpCommand.get_bot_mapping() | Get all command that are available in the bot, sort them by Cogs and None for No Category as key in a dictionary. This method is triggered before HelpCommand.send_bot_help is triggered, and will get passed as the parameter. |
HelpCommand.get_destination() | Returns a Messageable on where the help command was invoked. |
HelpCommand.command_callback | The method that handles all help/help cog/ help command/ help group and call which method appropriately. This is useful if you want to modify the behaviour of this. Though more knowledge is needed for you to do that. Most don't use this. |
Context.send_help() | Calling send_command_help based on what the Context command object were. This is useful to be used when the user incorrectly invoke the command. Which you can call this method to show help quickly and efficiently. (Only works if you have HelpCommand configured) |
All of this does not exist when you set bot.help_command
to None
. You miss out on this.
Since it's a class, most people would make it modular. They put it in a cog for example. There is a common code given in discord.py
created by Vex.
from discord.ext import commands
class MyHelpCommand(commands.MinimalHelpCommand):
pass
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self._original_help_command = bot.help_command
bot.help_command = MyHelpCommand()
bot.help_command.cog = self
def cog_unload(self):
self.bot.help_command = self._original_help_command
Well, first we have a HelpCommand
made there called MyHelpCommand
. When MyCog class is loaded,
bot.help_command
is stored into self._original_help_command
. This preserve the old help command that was attached to
the bot, and then it is assigned to a new HelpCommand
that you've made.
cog_unload
is triggered when the cog is unloaded, which assign bot.help_command
to the original help command.
For example, you have a custom help command that is currently attached to bot.help_command
. But you want to develop a
new help command or modify the existing without killing the bot. So you can just unload the cog, which will assign the old help command
to the bot so that you will always have a backup HelpCommand
ready while you're modifying and testing your custom help.
With that out of the way, let's get started. For subclassing HelpCommand, first, you would need to know the types of HelpCommand
.
Where each class has their own usage.
There are a few types of HelpCommand classes that you can choose;
DefaultHelpCommand
a help command that is given by default.MinimalHelpCommand
a slightly better help command.HelpCommand
an empty class that is the base class for every HelpCommand you see. On its own, it will not do anything.
By default, help command is using the class DefaultHelpCommand
. This is stored in bot.help_command
. This attribute
will ONLY accept instances that subclasses HelpCommand
. Here is how you were to use the DefaultHelpCommand
instance.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
bot.help_command = commands.DefaultHelpCommand()
# OR
help_command = commands.DefaultHelpCommand()
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", help_command=help_command, intents=intents)
# Both are equivalent
Now, of course, this is done by default. I'm only showing you this as a demonstration. Don't scream at me
Let's do the same thing with MinimalHelpCommand
next.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
bot.help_command = commands.MinimalHelpCommand()
Now say, you want the content to be inside an embed. But you don't want to change the content of
DefaultHelpCommand
/MinimalHelpCommand
since you want a simple HelpCommand with minimal work. There is a short code
from ?tag embed help example
by gogert
in discord.py server, a sample code you can follow
shows this;
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyNewHelp(commands.MinimalHelpCommand):
async def send_pages(self):
destination = self.get_destination()
for page in self.paginator.pages:
emby = discord.Embed(description=page)
await destination.send(embed=emby)
bot.help_command = MyNewHelp()
The resulting code will show that it have the content of MinimalHelpCommand
but in an embed.
Looking over the MinimalHelpCommand
source code,
every method that is responsible for <prefix>help <argument>
will call MinimalHelpCommand.send_pages
when it is about to send the content. This makes it easy to just override send_pages
without having to override any
other method there are in MinimalHelpCommand
.
If you want to use HelpCommand
class, we need to understand the basic of subclassing HelpCommand.
Here are a list of HelpCommand relevant methods, and it's responsibility.
HelpCommand.send_bot_help(mapping)
Gets called with<prefix>help
HelpCommand.send_command_help(command)
Gets called with<prefix>help <command>
HelpCommand.send_group_help(group)
Gets called with<prefix>help <group>
HelpCommand.send_cog_help(cog)
Gets called with<prefix>help <cog>
HelpCommand.context
the Context object in the help command.
For more, Click here
This is a bare minimum on what you should know on how a HelpCommand operate. As of discord version 1.* and 2.0. It remained the same flow.
Seems simple enough? Now let's see what happens if you override one of the methods. Here's an example code of how you
would do that. This override will say "hello!"
when you type <prefix>help
to demonstrate on what's going on.
We'll use HelpCommand.get_destination()
to get the abc.Messageable
instance for sending a message to the correct channel.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyHelp(commands.HelpCommand):
async def send_bot_help(self, mapping):
channel = self.get_destination()
await channel.send("hello!")
bot.help_command = MyHelp()
Keep in mind, using HelpCommand
class will require overriding every send_x_help
methods. For example, <prefix>help jsk
is
a command that should call send_command_help
method. However, since HelpCommand
is an empty class, it will not say
anything.
Let's work our way to create a <prefix> help
.
Given the documentation, await send_bot_help(mapping)
method receives mapping(Mapping[Optional[Cog], List[Command]])
as its parameter. await
indicates that it should be an async function.
Mapping[]
is acollections.abc.Mapping
, for simplicity’s sake, this usually refers to a dictionary since it's undercollections.abc.Mapping
.Optional[Cog]
is aCog
object that has a chance to beNone
.List[Command]
is a list ofCommand
objects.Mapping[Optional[Cog], List[Command]]
means it's a map object withOptional[Cog]
as it's key andList[Command]
as its value.
All of these are typehints in the typing
module. You can learn more about it here.
Now, for an example, we will use this mapping
given in the parameter of send_bot_help
. For each of the command, we'll
use HelpCommand.get_command_signature(command)
to get the command signature of a command in an str
form.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyHelp(commands.HelpCommand):
async def send_bot_help(self, mapping):
embed = discord.Embed(title="Help")
for cog, commands in mapping.items():
command_signatures = [self.get_command_signature(c) for c in commands]
if command_signatures:
cog_name = getattr(cog, "qualified_name", "No Category")
embed.add_field(name=cog_name, value="\n".join(command_signatures), inline=False)
channel = self.get_destination()
await channel.send(embed=embed)
bot.help_command = MyHelp()
- Create an embed.
- Use dict.items() to get an iterable of
(Cog, list[Command])
. - Each element in
list[Command]
, we will callself.get_command_signature(command)
to get the proper signature of the command. - If the list is empty, meaning, no commands is available in the cog, we don't need to show it, hence
if command_signatures:
. cog
has a chance to beNone
, this refers to No Category. We'll usegetattr
to avoid getting an error to get cog's name throughCog.qualified_name
.- Using
str.join
each command will be displayed on a separate line. - Once all of this is finished, display it.
This looks pretty... terrible. Those '|' are aliases of the command hence it appeared with a second command name. Let's make the signature prettier, and what if you wanna hide commands that you don't want to be shown on the help command? Such as "sync" command there, that's only for the developer not other people.
We'll subclass commands.MinimalHelpCommand
to use their MinimalHelpCommand.get_command_signature
. It's actually more
prettier than the default HelpCommand signature.
We'll use HelpCommand.filter_commands
,
this method will filter commands by removing any commands that the user cannot use. It is a handy method to use.
This works by checking if the Command.hidden
is set to True and, it will run Command.can_run
to see if it raise any errors. If there is any, it will be filtered.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyHelp(commands.MinimalHelpCommand):
async def send_bot_help(self, mapping):
embed = discord.Embed(title="Help")
for cog, commands in mapping.items():
filtered = await self.filter_commands(commands, sort=True)
command_signatures = [self.get_command_signature(c) for c in filtered]
if command_signatures:
cog_name = getattr(cog, "qualified_name", "No Category")
embed.add_field(name=cog_name, value="\n".join(command_signatures), inline=False)
channel = self.get_destination()
await channel.send(embed=embed)
bot.help_command = MyHelp()
This looks more readable than the other one with a small modification to the code.
While this should cover most of your needs, you may want to know more helpful
attribute that is available on HelpCommand
in the official documentation.
Now that the hard part is done, let's take a look at <prefix>help [argument]
. The method responsible for this is as
follows;
send_command_help
send_cog_help
send_group_help
As a demonstration, let's go for send_command_help
this method receive a Command
object. For this, it's simple, all
you have show is the attribute of the command.
For example, this is your command code, your goal is you want to show the help
,aliases
and the signature
.
@bot.command(help="Generic help command for command hello.",
aliases=["h", "hellos", "hell", "hehe"])
async def hello(ctx, bot: discord.Member):
pass
Then it's simple, you can display each of the attribute by Command.help
and Command.aliases
.
For the signature, instead of using the previous get_command_signature
, we're going to subclass MinimalHelpCommand
.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyHelp(commands.MinimalHelpCommand):
async def send_command_help(self, command):
embed = discord.Embed(title=self.get_command_signature(command))
embed.add_field(name="Help", value=command.help)
alias = command.aliases
if alias:
embed.add_field(name="Aliases", value=", ".join(alias), inline=False)
channel = self.get_destination()
await channel.send(embed=embed)
bot.help_command = MyHelp()
As you can see, it is very easy to create <prefix>help [argument]
. The class already handles the pain of checking whether
the given argument is a command, a cog, or a group command. It's up to you on how you want to display it, whether it's through
a plain message, an embed or even using discord.ext.menus
.
Let's say, someone is spamming your help command. For a normal command, all you have to do to combat this is using a
cooldown
decorator
and slap that thing above the command declaration. Or, what about if you want an alias? Usually, you would put an
aliases kwargs in the command
decorator. However, HelpCommand is a bit special, It's a god damn class. You can't just put a decorator on it and expect
it to work.
That is when HelpCommand.command_attrs
come to the rescue. This attribute can be set during the HelpCommand declaration, or a direct attribute assignment.
According to the documentation, it accepts exactly the same thing as a command
decorator in a form of a dictionary.
For example, we want to rename the help command as "hell"
instead of "help"
for whatever reason. We also want to make
an alias for "help"
so users can call the command with "hell"
and "help"
. Finally, we want to put a cooldown,
because help command messages are big, and we don't want people to spam those. So, what would the code look like?
import discord
from discord.ext import commands
intents = discord.Intents.all()
attributes = {
'name': "hell",
'aliases': ["help", "helps"],
'cooldown': commands.CooldownMapping.from_cooldown(2, 5.0, commands.BucketType.user)
}
# During declaration
help_object = commands.MinimalHelpCommand(command_attrs=attributes)
# OR through attribute assignment
help_object = commands.MinimalHelpCommand()
help_object.command_attrs = attributes
bot = commands.Bot(command_prefix="uwu ", help_command=help_object, intents=intents)
- sets the name into
"hell"
is refers to here'name': "hell"
. - sets the aliases by passing the list of
str
to thealiases
key, which refers to here'aliases': ["help", "helps"]
. - sets the cooldown through the
"cooldown"
key by passing in aCooldownMapping
object. This object will make a cooldown with a rate of 2, per 5 with a bucket typeBucketType.user
, which in simple terms, for everydiscord.User
, they can call the command twice, every 5 seconds. - We're going to use
MinimalHelpCommand
as theHelpCommand
object.
Note: on Number 3, Cooldown has been updated on 2.0. Please check the code on your own instead.
As you can see, the name of the help command is now "hell"
, and you can also trigger the help command by "help"
. It
will also raise an OnCommandCooldown
error if it was triggered 3 times in 5 seconds due to our Cooldown
object. Of course, I didn't show that in the result,
but you can try it yourself. You should handle the error in an error handler when an OnCommandCooldown
is raised.
What happens when <prefix>help command
fails to get a command/cog/group? Simple, HelpCommand.send_error_message
will
be called. HelpCommand will not call on_command_error
when it can't find an existing command. It will also give you an
str
instead of an error instance.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyHelp(commands.HelpCommand):
async def send_error_message(self, error):
embed = discord.Embed(title="Error", description=error)
channel = self.get_destination()
await channel.send(embed=embed)
bot.help_command = MyHelp()
error
is a string that will only contain the message, all you have to do is display the message.
Indeed, we have it. HelpCommand.on_help_command_error
,
this method is responsible for handling any error just like any other local error handler.
import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
class MyHelp(commands.HelpCommand):
async def send_bot_help(self, mapping):
raise commands.BadArgument("Something broke")
async def on_help_command_error(self, ctx, error):
if isinstance(error, commands.BadArgument):
embed = discord.Embed(title="Error", description=str(error))
await ctx.send(embed=embed)
else:
raise error
bot.help_command = MyHelp()
Rather basic, just raise an error that subclasses commands.CommandError
such as commands.BadArgument
. The error raised
will cause on_help_command_error
to be invoked. The code shown will catch this commands.BadArgument
instance that is
stored in error
variable, and show the message.
To be fair, you should create a proper error handler through this official documentation. Here.
There is also a lovely example by Mysty
on error handling in general. Here.
This example shows how to properly create a global error handler and local error handler.
It works just like setting a cog on a Command object, you basically have to assign a commands.Cog
instance into HelpCommand.cog
.
It is pretty common for discord.py users to put a HelpCommand into a cog/separate file since people want organization.
This code example is if you're in a Cog file, which you have access to a Cog instance
from discord.ext import commands
# Unimportant part
class MyHelp(commands.HelpCommand):
async def send_bot_help(self, mapping):
channel = self.get_destination()
await channel.send("hey")
class YourCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Focus here
# Setting the cog for the help
help_command = MyHelp()
help_command.cog = self # Instance of YourCog class
bot.help_command = help_command
async def setup(bot):
await bot.add_cog(YourCog(bot))
- It instantiates the HelpCommand class.
help_command = MyHelp()
- It assigns the instance of
YourCog
(self) intocog
attribute. When you assign aCog
on a HelpCommand, discord.py will automatically know that the HelpCommand belongs to thatCog
. It was stated here.
I hope that reading this walkthrough will assist you and give a better understanding on how to subclass HelpCommand
.
All the example code given are to demonstrate the feature of HelpCommand
and feel free to try it. There are lots of
creative things you can do to create a HelpCommand
.
If you want a generic help command, here's an example of a help command written by pikaninja Here's the code Here's how it looks like.
For my implementation, its a bit complex. I wrote a library for myself that I use in several bots. You can see the codes through this repository. Which you're free to use if you want a quick setup for your help command.
Looks like this
Now, of course, any question regarding HelpCommand
should be asked in the discord.py server
because I don't really check this gist as much, and because there is a lot of helpful discord.py helpers if you're nice
enough to them.
Hyperlinks are updated now, I was waiting for v2 to be released but it seems kinda long to wait for it.