aboutsummaryrefslogtreecommitdiff
path: root/docs/ext/commands/commands.rst
diff options
context:
space:
mode:
authorRapptz <[email protected]>2017-07-08 00:27:11 -0400
committerRapptz <[email protected]>2017-07-08 00:28:23 -0400
commit2f97678a79652e7d00118e6843feda3885795675 (patch)
tree1f983214568aad5c605c44d203d9dedee5b05445 /docs/ext/commands/commands.rst
parentFixed broken code (diff)
downloaddiscord.py-2f97678a79652e7d00118e6843feda3885795675.tar.xz
discord.py-2f97678a79652e7d00118e6843feda3885795675.zip
First pass at commands narrative documentation.
Diffstat (limited to 'docs/ext/commands/commands.rst')
-rw-r--r--docs/ext/commands/commands.rst584
1 files changed, 584 insertions, 0 deletions
diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst
new file mode 100644
index 00000000..81a7de0b
--- /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 <https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range>`_,
+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 <ext_commands_error_handler>`.
+
+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 <ext_commands_error_handler>`. 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