Command Handling

The biggest use case of nearly every bot is to run actions in response to commands. Curious comes with built-in commands managers for this purpose.

The Commands Manager

The CommandsManager is the main way to attach commands to the client.

First, you need to create the manager and attach it to a client:

# form 1, automatically register with the client
manager = CommandsManager.with_client(bot)

# form 2, manually register
manager = CommandsManager(bot)
manager.register_events()

This is required to add the handler events to the client.

Next, you need to register a message check handler. This is a callable that is called for every message to try and extract the command from a message, if it matches. By default, the manager provides an easy way to use a simple command prefix:

manager = CommandsManager(bot, command_prefix="!")

At this point, the command prefix will be available on the manager with Manager.message_check.prefix.

If you need more complex message checking, you can use message_check:

manager = CommandsManager(bot, message_check=my_message_checker)
# or
manager.message_check = my_message_checker

Plugins

Plugins are a simple way of extending your bot. They come in the form of classes containing commands. All plugins are derived from Plugin.

from curious.commands.plugin import Plugin

class MyPlugin(Plugin):
    ...

Commands can be created with the usage of the command() decorator:

from curious.commands.decorators import command
from curious.commands.context import Context

class MyPlugin(Plugin):
    @command()
    async def pong(self, ctx: Context):
        await ctx.channel.messages.send("Ping!")

You can register plugins or modules containing plugins with the manager:

@bot.event("ready")
async def load_plugins(ctx: EventContext):
    # load plugin explicitly
    await manager.load_plugin(PluginClass, arg1)
    # load plugins from a module
    await manager.load_plugins_from("my.plugin.module")

Commands

Commands are a way of running an isolated block of code in response to a user sending a message with a prefix. Commands can be created with the command() decorator which will automatically annotate the function with some metadata that marks it as a command.

@command()
async def ping(self, ctx: Context):
    await ctx.channel.messages.send("Pong!")

The command decorator takes several arguments to customize the behaviour of the command outside of the code inside the function; see the decorator docstring for more information.

Subcommands

Curious supports subcommands natively, using a small amount of metaprogramming magic. To create a subcommand, simply use the parent.subcommand() function as a decorator on your command, like so:

@command()
async def say(self, ctx: Context):
    # this only runs if no subcommand was provided by the user
    await ctx.channel.messages.send(":x: What do you want me to say?")

@say.subcommand()
async def hello(self, ctx: Context):
    await ctx.channel.messages.send("Hello!")

Subcommands can be nested infinitely deep; you can have subcommands of subcommands down to any level.

Context

Warning

This is subject to change in newer versions due to ContextVar support.

The Context object is a powerful object when using commands; as well as containing some internal machinery used to run commands it also provides an interface to the context of the command, i.e. the server/channel/author for the command, and so on.

Some useful attributes on the context object:

  • Context.channel - The Channel object that the command was sent in.
  • Context.author - The Member or User that sent the command.
  • Context.guild - The Guild object the command was sent in. May be None.
  • Context.bot - The reference to the bot that the command was handled by.
  • Context.event_context - The EventContext used internally for the command.

Arguments

Arguments to commands are consumed in a specific way, according to the function signature:

  • Positional arguments are consumed from single words or single blocks of quoted words, passing through a single string per argument.
  • *args arguments consume every single word, passing through a list.
  • *, argument arguments also consume every single word, passing through a joined string.
  • Keyword arguments are consumed, but use their default value if not found.
  • **kwargs is ignored.

This means that a function with the signature (arg1, arg2, *, arg3), when fed the input of "test1 test2 test3 test4" would result in {arg1: test1, arg2: test2, arg3: test3 test4}.

Additionally, arguments can be typed; this allows automatic conversion from the string input to the appropriate type for your function. This is achieved through the usage of standard Python 3 type annotations on the arguments. Some built-in converters are provided:

  • arg: int - converts the argument into an integer.
  • arg: float - converts the argument into a float.
  • arg: Channel - converts the argument into a Channel.
  • arg: Member - converts the argument into a Member.
  • arg: Role - converts the argument into a Role.

Some more advanced converters are supported too:

  • arg: List[T] - converts the argument into a list of T.
  • arg: Union[T1, T2] - converts the argument into either T1 or T2.

Additional converters can be added by calling Context.add_converter(); the converter must be a simple callable that takes a pair of arguments (ctx, arg) and returns the appropriate type.

Conditions

Conditions are a way to ensure that a command only runs under certain circumstances. A condition can be added to a command with the usage of the condition() decorator:

@command()
@condition(lambda ctx: ctx.guild.id == 198101180180594688)
async def secret_command(self, ctx): ...

The argument to condition must be a callable that takes one argument, a Context object, and returns True if the command will run and False otherwise. If an exception is raised, it will be trapped and the command will not run (similar to returning False).

Free-standing commands

You can also add free-standing commands that aren’t bound to a plugin with CommandsManager.add_command():

@command()
async def ping(ctx: Context):
    await ctx.channel.send(content="Pong!")

manager.add_command(ping)

These will then be available to the client.

Background Tasks

Background tasks are async functions that run in the background; i.e. you don’t have to await them. curious provides an easy, portable way to spawn a background task from a Plugin, using Plugin.spawn():

async def my_task(self):
    while True:
        print("Ok!")
        await trio.sleep(300)

@command()
async def spawn(self, ctx):
    await self.spawn(self.my_task)

This task will be parented to the plugin’s task group, which is parented to the client’s root task group. Exceptions will automatically be swallowed and logged, to prevent crashing the whole bot.