diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 34ab30e2c..893719a17 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -276,7 +276,7 @@ class BotBase(GroupMixin): The function that was used as a global check. call_once: bool If the function should only be called once per - :meth:`.invoke` call. + :meth:`.Command.invoke` call. """ if call_once: @@ -308,7 +308,7 @@ class BotBase(GroupMixin): """A decorator that adds a "call once" global check to the bot. Unlike regular global checks, this one is called only once - per :meth:`.invoke` call. + per :meth:`.Command.invoke` call. Regular global checks are called whenever a command is called or :meth:`.Command.can_run` is called. This type of check diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst new file mode 100644 index 000000000..81a7de0bb --- /dev/null +++ b/docs/ext/commands/commands.rst @@ -0,0 +1,584 @@ +.. currentmodule:: discord + +.. _ext_commands_commands: + +Commands +========== + +One of the most appealing aspect of the command extension is how easy it is to define commands and +how you can arbitrarily nest groups and commands to have a rich sub-command system. + +Commands are defined by attaching it to a regular Python function. The command is then invoked by the user using a similar +signature to the Python function. + +For example, in the given command definition: + +.. code-block:: python3 + + @bot.command() + async def foo(ctx, arg): + await ctx.send(arg) + +With the following prefix (``$``), it would be invoked by the user via: + +.. code-block:: none + + $foo abc + +A command must always have at least one parameter, ``ctx``, which is the :class:`.Context` as the first one. + +There are two ways of registering a command. The first one is by using :meth:`.Bot.command` decorator, +as seen in the example above. The second is using the :func:`~ext.commands.command` decorator followed by +:meth:`.Bot.add_command` on the instance. + +Essentially, these two are equivalent: :: + + from discord.ext import commands + + bot = commands.Bot(command_prefix='$') + + @bot.command() + async def test(ctx): + pass + + # or: + + @commands.command() + async def test(ctx): + pass + + bot.add_command(test) + +Since the :meth:`.Bot.command` decorator is shorter and easier to comprehend, it will be the one used throughout the +documentation here. + +Any parameter that is accepted by the :class:`.Command` constructor can be passed into the decorator. For example, to change +the name to something other than the function would be as simple as doing this: + +.. code-block:: python3 + + @bot.command(name='list') + async def _list(ctx, arg): + pass + +Parameters +------------ + +Since we define commands by making Python functions, we also define the argument passing behaviour by the function +parameters. + +Certain parameter types do different things in the user side and most forms of parameter types are supported. + +Positional +++++++++++++ + +The most basic form of parameter passing is the positional parameter. This is where we pass a parameter as-is: + +.. code-block:: python3 + + @bot.command() + async def test(ctx, arg): + await ctx.send(arg) + + +On the bot using side, you can provide positional arguments by just passing a regular string: + +.. image:: /images/commands/positional1.png + +To make use of a word with spaces in between, you should quote it: + +.. image:: /images/commands/positional2.png + +As a note of warning, if you omit the quotes, you will only get the first word: + +.. image:: /images/commands/positional3.png + +Since positional arguments are just regular Python arguments, you can have as many as you want: + +.. code-block:: python3 + + @bot.command() + async def test(ctx, arg1, arg2): + await ctx.send('You passed {} and {}'.format(arg1, arg2)) + +Variable +++++++++++ + +Sometimes you want users to pass in an undetermined number of parameters. The library supports this +similar to how variable list parameters are done in Python: + +.. code-block:: python3 + + @bot.command() + async def test(ctx, *args): + await ctx.send('{} arguments: {}'.format(len(args), ', '.join(args))) + +This allows our user to accept either one or many arguments as they please. This works similar to positional arguments, +so multi-word parameters should be quoted. + +For example, on the bot side: + +.. image:: /images/commands/variable1.png + +If the user wants to input a multi-word argument, they have to quote it like earlier: + +.. image:: /images/commands/variable2.png + +Do note that similar to the Python function behaviour, a user can technically pass no arguments +at all: + +.. image:: /images/commands/variable3.png + +Since the ``args`` variable is a `list `_, +you can do anything you would usually do with one. + +Keyword-Only Arguments +++++++++++++++++++++++++ + +When you want to handle parsing of the argument yourself or do not feel like you want to wrap multi-word user input into +quotes, you can ask the library to give you the rest as a single argument. We do this by using a **keyword-only argument**, +seen below: + +.. code-block:: python3 + + @bot.command() + async def test(ctx, *, arg): + await ctx.send(arg) + +.. warning:: + + You can only have one keyword-only argument due to parsing ambiguities. + +On the bot side, we do not need to quote input with spaces: + +.. image:: /images/commands/keyword1.png + +Do keep in mind that wrapping it in quotes leaves it as-is: + +.. image:: /images/commands/keyword2.png + +By default, the keyword-only arguments are stripped of white space to make it easier to work with. This behaviour can be +toggled by the :attr:`.Command.rest_is_raw` argument in the decorator. + +.. _ext_commands_context: + +Invocation Context +------------------- + +As seen earlier, every command must take at least a single parameter, called the :class:`~ext.commands.Context`. + +This parameter gives you access to something called the "invocation context". Essentially all the information you need to +know how the command was executed. It contains a lot of useful information: + +- :attr:`.Context.guild` to fetch the :class:`Guild` of the command, if any. +- :attr:`.Context.message` to fetch the :class:`Message` of the command. +- :attr:`.Context.author` to fetch the :class:`Member` or :class:`User` that called the command. +- :meth:`.Context.send` to send a message to the channel the command was used in. + +The context implements the :class:`abc.Messageable` interface, so anything you can do on a :class:`abc.Messageable` you +can do on the :class:`~ext.commands.Context`. + +Converters +------------ + +Adding bot arguments with function parameters is only the first step in defining your bot's command interface. To actually +make use of the arguments, we usually want to convert the data into a target type. We call these +:ref:`ext_commands_api_converters`. + +Converters come in a few flavours: + +- A regular callable object that takes an argument as a sole parameter and returns a different type. + + - These range from your own function, to something like ``bool`` or ``int``. + +- A custom class that inherits from :class:`~ext.commands.Converter`. + +Basic Converters +++++++++++++++++++ + +At its core, a basic converter is a callable that takes in an argument and turns it into something else. + +For example, if we wanted to add two numbers together, we could request that they are turned into integers +for us by specifying the converter: + +.. code-block:: python3 + + @bot.command() + async def add(ctx, a: int, b: int): + await ctx.send(a + b) + +We specify converters by using something called a **function annotation**. This is a Python 3 exclusive feature that was +introduced in :pep:`3107`. + +This works with any callable, such as a function that would convert a string to all upper-case: + +.. code-block:: python3 + + def to_upper(argument): + return argument.upper() + + @bot.command() + async def up(ctx, *, content: to_upper): + await ctx.send(content) + +.. _ext_commands_adv_converters: + +Advanced Converters ++++++++++++++++++++++ + +Sometimes a basic converter doesn't have enough information that we need. For example, sometimes we want to get some +information from the :class:`Message` that called the command or we want to do some asynchronous processing. + +For this, the library provides the :class:`~ext.commands.Converter` interface. This allows you to have access to the +:class:`.Context` and have the callable be asynchronous. Defining a custom converter using this interface requires +overriding a single method, :meth:`.Converter.convert`. + +An example converter: + +.. code-block:: python3 + + import random + + class Slapper(commands.Converter): + async def convert(self, ctx, argument): + to_slap = random.choice(ctx.guild.members) + return '{0.author} slapped {1} because *{2}*'.format(ctx, to_slap, argument) + + @bot.command() + async def slap(ctx, *, reason: Slapper): + await ctx.send(reason) + +The converter provided can either be constructed or not. Essentially these two are equivalent: + +.. code-block:: python3 + + @bot.command() + async def slap(ctx, *, reason: Slapper): + await ctx.send(reason) + + # is the same as... + + @bot.command() + async def slap(ctx, *, reason: Slapper()): + await ctx.send(reason) + +Having the possibility of the converter be constructed allows you to set up some state in the converter's ``__init__`` for +fine tuning the converter. An example of this is actually in the library, :class:`~ext.commands.clean_content`. + +.. code-block:: python3 + + @bot.command() + async def clean(ctx, *, content: commands.clean_content): + await ctx.send(content) + + # or for fine-tuning + + @bot.command() + async def clean(ctx, *, content: commands.clean_content(use_nicknames=False)): + await ctx.send(content) + + +If a converter fails to convert an argument to its designated target type, the :exc:`.BadArgument` exception must be +raised. + +Discord Converters +++++++++++++++++++++ + +Working with :ref:`discord_api_models` is a fairly common thing when defining commands, as a result the library makes +working with them easy. + +For example, to receive a :class:`Member`, you can just pass it as a converter: + +.. code-block:: python3 + + @bot.command() + async def joined(ctx, *, member: discord.Member): + await ctx.send('{0} joined on {0.joined_at}'.format(member)) + +When this command is executed, it attempts to convert the string given into a :class:`Member` and then passes it as a +parameter for the function. This works by checking if the string is a mention, an ID, a nickname, a username + discriminator, +or just a regular username. The default set of converters have been written to be as easy to use as possible. + +A lot of discord models work out of the gate as a parameter: + +- :class:`Member` +- :class:`User` +- :class:`TextChannel` +- :class:`VoiceChannel` +- :class:`Role` +- :class:`Invite` +- :class:`Game` +- :class:`Emoji` +- :class:`Colour` + +Having any of these set as the converter will intelligently convert the argument to the appropriate target type you +specify. + +Under the hood, these are implemented by the :ref:`ext_commands_adv_converters` interface. A table of the equivalent +converter is given below: + ++-----------------------+----------------------------------------------+ +| Discord Class | Converter | ++-----------------------+----------------------------------------------+ +| :class:`Member` | :class:`~ext.commands.MemberConverter` | ++-----------------------+----------------------------------------------+ +| :class:`User` | :class:`~ext.commands.UserConverter` | ++-----------------------+----------------------------------------------+ +| :class:`TextChannel` | :class:`~ext.commands.TextChannelConverter` | ++-----------------------+----------------------------------------------+ +| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` | ++-----------------------+----------------------------------------------+ +| :class:`Role` | :class:`~ext.commands.RoleConverter` | ++-----------------------+----------------------------------------------+ +| :class:`Invite` | :class:`~ext.commands.InviteConverter` | ++-----------------------+----------------------------------------------+ +| :class:`Game` | :class:`~ext.commands.GameConverter` | ++-----------------------+----------------------------------------------+ +| :class:`Emoji` | :class:`~ext.commands.EmojiConverter` | ++-----------------------+----------------------------------------------+ +| :class:`Colour` | :class:`~ext.commands.ColourConverter` | ++-----------------------+----------------------------------------------+ + +By providing the converter it allows us to use them as building blocks for another converter: + +.. code-block:: python3 + + class MemberRoles(commands.MemberConverter): + async def convert(self, ctx, argument): + member = await super().convert(ctx, argument) + return member.roles + + @bot.command() + async def roles(ctx, *, member: MemberRoles): + """Tells you a member's roles.""" + await ctx.send('I see the following roles: ' + ', '.join(member)) + +Inline Advanced Converters ++++++++++++++++++++++++++++++ + +If we don't want to inherit from :class:`~ext.commands.Converter`, we can still provide a converter that has the +advanced functionalities of an advanced converter and save us from specifying two types. + +For example, a common idiom would be to have a class and a converter for that class: + +.. code-block:: python3 + + class JoinDistance: + def __init__(self, joined, created): + self.joined = joined + self.created = created + + @property + def delta(self): + return self.joined - self.created + + class JoinDistanceConverter(commands.MemberConverter): + async def convert(self, ctx, argument): + member = await super().convert(ctx, argument) + return JoinDistance(member.joined_at, member.created_at) + + @bot.command() + async def delta(ctx, *, member: JoinDistanceConverter): + is_new = member.delta.days < 100 + if is_new: + await ctx.send("Hey you're pretty new!") + else: + await ctx.send("Hm you're not so new.") + +This can get tedious, so an inline advanced converter is possible through a ``classmethod`` inside the type: + +.. code-block:: python3 + + class JoinDistance: + def __init__(self, joined, created): + self.joined = joined + self.created = created + + @classmethod + async def convert(cls, ctx, argument): + member = await commands.MemberConverter().convert(ctx, argument) + return cls(member.joined_at, member.created_at) + + @property + def delta(self): + return self.joined - self.created + + @bot.command() + async def delta(ctx, *, member: JoinDistance): + is_new = member.delta.days < 100 + if is_new: + await ctx.send("Hey you're pretty new!") + else: + await ctx.send("Hm you're not so new.") + +.. _ext_commands_error_handler: + +Error Handling +---------------- + +When our commands fail to either parse we will, by default, receive a noisy error in ``stderr`` of our console that tells us +that an error has happened and has been silently ignored. + +In order to handle our errors, we must use something called an error handler. There is a global error handler, called +:func:`on_command_error` which works like any other event in the :ref:`discord-api-events`. This global error handler is +called for every error reached. + +Most of the time however, we want to handle an error local to the command itself. Luckily, commands come with local error +handlers that allow us to do just that. First we decorate an error handler function with :meth:`.Command.error`: + +.. code-block:: python3 + + @bot.command() + async def info(ctx, *, member: discord.Member): + """Tells you some info about the member.""" + fmt = '{0} joined on {0.joined_at} and has {1} roles.' + await ctx.send(fmt.format(member, len(member.roles))) + + @info.error + async def info_error(ctx, error): + if isinstance(error, commands.BadArgument): + await ctx.send('I could not find that member...') + +The first parameter of the error handler is the :class:`.Context` while the second one is an exception that is derived from +:exc:`~ext.commands.CommandError`. A list of errors is found in the :ref:`ext_commands_api_errors` page of the documentation. + +Checks +------- + +There are cases when we don't want a user to use our commands. They don't have permissions to do so or maybe we blocked +them from using our bot earlier. The commands extension comes with full support for these things in a concept called a +:ref:`ext_commands_api_checks`. + +A check is a basic predicate that can take in a :class:`.Context` as its sole parameter. Within it, you have the following +options: + +- Return ``True`` to signal that the person can run the command. +- Return ``False`` to signal that the person cannot run the command. +- Raise a :exc:`~ext.commands.CommandError` derived exception to signal the person cannot run the command. + + - This allows you to have custom error messages for you to handle in the + :ref:`error handlers `. + +To register a check for a command, we would have two ways of doing so. The first is using the :meth:`~ext.commands.check` +decorator. For example: + +.. code-block:: python3 + + async def is_owner(ctx): + return ctx.author.id == 316026178463072268 + + @bot.command(name='eval') + @commands.check(is_owner) + async def _eval(ctx, *, code): + """A bad example of an eval command""" + await ctx.send(eval(code)) + +This would only evaluate the command if the function ``is_owner`` returns ``True``. Sometimes we re-use a check often and +want to split it into its own decorator. To do that we can just add another level of depth: + +.. code-block:: python3 + + def is_owner(): + async def predicate(ctx): + return ctx.author.id == 316026178463072268 + return commands.check(predicate) + + @bot.command(name='eval') + @is_owner() + async def _eval(ctx, *, code): + """A bad example of an eval command""" + await ctx.send(eval(code)) + + +Since an owner check is so common, the library provides it for you (:func:`~ext.commands.is_owner`): + +.. code-block:: python3 + + @bot.command(name='eval') + @commands.is_owner() + async def _eval(ctx, *, code): + """A bad example of an eval command""" + await ctx.send(eval(code)) + +When multiple checks are specified, **all** of them must be ``True``: + +.. code-block:: python3 + + def is_in_guild(guild_id): + async def predicate(ctx): + return ctx.guild and ctx.guild.id == guild_id + return commands.check(is_in_guild) + + @bot.command() + @is_in_guild(41771983423143937) + async def secretguilddata(ctx): + """super secret stuff""" + await ctx.send('secret stuff') + +If any of those checks fail in the example above, then the command will not be run. + +When an error happens, the error is propagated to the :ref:`error handlers `. If you do not +raise a custom :exc:`~ext.commands.CommandError` derived exception, then it will get wrapped up into a +:exc:`~ext.commands.CheckFailure` exception as so: + +.. code-block:: python3 + + @bot.command() + @is_in_guild(41771983423143937) + async def secretguilddata(ctx): + """super secret stuff""" + await ctx.send('secret stuff') + + @secretguilddata.error + async def secretguilddata_error(ctx, error): + if isinstance(error, commands.CheckFailure): + await ctx.send('nothing to see here comrade.') + +If you want a more robust error system, you can derive from the exception and raise it instead of returning ``False``: + +.. code-block:: python3 + + class NoPrivateMessages(commands.CheckFailure): + pass + + def guild_only(): + async def predicate(ctx): + if ctx.guild is None: + raise NoPrivateMessages('Hey no DMs!') + return True + return commands.check(predicate) + + @guild_only() + async def test(ctx): + await ctx.send('Hey this is not a DM! Nice.') + + @test.error + async def test_error(ctx, error): + if isinstance(error, NoPrivateMessages): + await ctx.send(error) + +.. note:: + + Since having a ``guild_only`` decorator is pretty common, it comes built-in via :func:`~ext.commands.guild_only`. + +Global Checks +++++++++++++++ + +Sometimes we want to apply a check to **every** command, not just certain commands. The library supports this as well +using the global check concept. + +Global checks work similarly to regular checks except they are registered with the :func:`.Bot.check` decorator. + +For example, to block all DMs we could do the following: + +.. code-block:: python3 + + @bot.check + async def globally_block_dms(ctx): + return ctx.guild is not None + +.. warning:: + + Be careful on how you write your global checks, as it could also lock you out of your own bot. + +.. need a note on global check once here I think diff --git a/docs/ext/commands/index.rst b/docs/ext/commands/index.rst index 908a7bc15..559597c3b 100644 --- a/docs/ext/commands/index.rst +++ b/docs/ext/commands/index.rst @@ -8,6 +8,7 @@ extension library that handles this for you. .. toctree:: - :maxdepth: 1 + :maxdepth: 2 + commands api diff --git a/docs/images/commands/keyword1.png b/docs/images/commands/keyword1.png new file mode 100644 index 000000000..d9f952345 Binary files /dev/null and b/docs/images/commands/keyword1.png differ diff --git a/docs/images/commands/keyword2.png b/docs/images/commands/keyword2.png new file mode 100644 index 000000000..40ce6764a Binary files /dev/null and b/docs/images/commands/keyword2.png differ diff --git a/docs/images/commands/positional1.png b/docs/images/commands/positional1.png new file mode 100644 index 000000000..a911be79d Binary files /dev/null and b/docs/images/commands/positional1.png differ diff --git a/docs/images/commands/positional2.png b/docs/images/commands/positional2.png new file mode 100644 index 000000000..7e1099fc5 Binary files /dev/null and b/docs/images/commands/positional2.png differ diff --git a/docs/images/commands/positional3.png b/docs/images/commands/positional3.png new file mode 100644 index 000000000..8da621e43 Binary files /dev/null and b/docs/images/commands/positional3.png differ diff --git a/docs/images/commands/variable1.png b/docs/images/commands/variable1.png new file mode 100644 index 000000000..4738368d3 Binary files /dev/null and b/docs/images/commands/variable1.png differ diff --git a/docs/images/commands/variable2.png b/docs/images/commands/variable2.png new file mode 100644 index 000000000..512cc6d3c Binary files /dev/null and b/docs/images/commands/variable2.png differ diff --git a/docs/images/commands/variable3.png b/docs/images/commands/variable3.png new file mode 100644 index 000000000..0b740c4e9 Binary files /dev/null and b/docs/images/commands/variable3.png differ diff --git a/docs/index.rst b/docs/index.rst index 9f0a4e34f..ba8c22164 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,7 +36,7 @@ Extensions ----------- .. toctree:: - :maxdepth: 2 + :maxdepth: 3 ext/commands/index.rst