Skip to content

Commands#

Slash commands are the commands you'll see in Discord when you start by "/" in the message box.

This guide is only going to cover using a command client to make commands as, while you can make commands with just Hikari, command clients makes it a lot easier and are the recommended approach.

These examples will be using Tanjun. Tanjun is a command framework which wraps around Hikari to provide ways declare and handle both modern application (slash) commands, traditional message (prefix) commands, and context menus.

Making Slash commands#

@tanchan.doc_parse.with_annotated_args
@tanchan.doc_parse.as_slash_command(
    default_to_ephemeral=True,
    dm_enabled=False,
    default_member_permissions=hikari.Permissions.BAN_MEMBERS,
)
async def ban_user(
    ctx: tanjun.abc.SlashContext,
    user: annotations.User,
    reason: annotations.Str | hikari.UndefinedType = hikari.UNDEFINED,
) -> None:
    """Ban a user.

    Parameters
    ----------
    user
        The user to ban.
    reason
        Why they're being banned.
    """
    # guild_id should never be None thanks to dm_enabled=False
    assert ctx.guild_id is not None

    try:
        await ctx.rest.ban_member(ctx.guild_id, user, reason=reason)

    except hikari.NotFoundError:
        await ctx.respond("User doesn't exist!!!")

    except hikari.ForbiddenError:
        await ctx.respond("I Cannot do that!!!")

    else:
        await ctx.respond(f"Successfully banned {user}")

first slash command example second slash command example

There's a few things going on in this example:

First off, tanchan.doc_parse.as_slash_command is used to create a slash command by wrapping a command callback. This is using the callback's name ("ban_user") as the command's name and using the first line of its docstring as the slash command's description.

Then tanchan.doc_parse.with_annotated_args parses the function's parameter type-hints to work out the slash command's options while parsing per-option descriptions from the docstring's "Parameters" section.

This example uses default_member_permissions to mark the command as limited to only members who have permission to ban users unless this configuration is overridden by guild staff. It also uses dm_only to marks the command as only working in guilds.

It should be noted that Tanchan is a separate optional utility library which builds on top of the system tanjun provides in tanjun.annotations to add support for docstring parsing.

Ephemeral responses#

The "only you can see this" responses you're seeing here are called ephemeral responses, these are unique to slash and context menu commands. While this example uses default_to_ephemeral=True to mark all the command's responses as ephemeral, you can also pass ephemeral=True while calling AppCommandContext.create_initial_response or AppCommandContext.create_followup to mark individual responses as ephemeral.

Slash command groups#
slash_command_group = tanchan.doc_parse.slash_command_group(
    "group", "description"
)

@slash_command_group.as_sub_command()
async def name(ctx: tanjun.abc.SlashContext) -> None:
    """`group name` command."""
    await ctx.respond("Called `group name` command")

slash command group example

Since normal slash commands can't have spaces in their names, you have to use slash command "groups" to get spaces. These are limited to only being nested once (so a slash command group can be put in a top-level group but you can't put a group in that sub-group) and some configuration is limited to top level commands so for what you can configure for sub-commands see SlashCommandGroup.as_sub_command and SlashCommandGroup.make_sub_group.

Making message commands#

Message commands are the classic command approach which listens for user messages which start with a certain prefix or bot mention and then processes the rest of the message's content for command execution.

The {GUILD}|{DM}_MESSAGES intent has to be enabled for these commands to work and without the privileged hikari.Intents.MESSAGE_CONTENT intent command execution will only work for mention prefixes.

Since these match based on the prefix, you must also set some prefix for the client to match against using either tanjun.clients.Client.add_prefix or by passing mention_prefix=True when initialising the bot (calling Client.from_gateway_bot).

@tanjun.annotations.with_annotated_args
@tanjun.as_message_command("meow")
async def message_command(
    ctx: tanjun.abc.MessageContext, weebish: tanjun.annotations.Bool = False
) -> None: ...

The above example shows a message command that would be triggered by {prefix}meow and where the "weebish" argument can optionally be provided by sending {prefix}meow --weebish true.

@tanjun.as_message_command_group("uwu group")
async def message_command_group(ctx: tanjun.abc.MessageContext) -> None: ...

@tanjun.annotations.with_annotated_args
@message_command_group.as_sub_command("echo")
async def sub_command(
    ctx: tanjun.abc.MessageContext,
    content: typing.Annotated[annotations.Str, annotations.Greedy()],
) -> None: ...

The above example takes advantage of message command groups to group sub-commands together. The group itself will be callable as {prefix}uwu group and the sub-command will be callable as {prefix}uwu group echo {content...}. Greedy marks this argument as taking the rest of the positional arguments and doesn't effect slash commands.

Making context menus#

Context menu commands are the simplest of the 3. These are the buttons you see when you right click on a user or message under "Apps". These have similar configuration to slash commands but cannot be nested, have more relaxed name restrictions and do not have descriptions nor options.

Message context menus#
@tanjun.as_message_menu("Archive", dm_enabled=False)
async def message_menu(
    ctx: tanjun.abc.MenuContext, message: hikari.Message
) -> None:
    # ... Archive logic
    try:
        await ctx.rest.delete_message(ctx.channel_id, message)

    except hikari.NotFoundError:
        pass

    except hikari.ForbiddenError:
        await ctx.respond("Couldn't delete message", delete_after=30)
        return

    await ctx.respond("Archived message", delete_after=30)

message menu example

There's message context menus which will always take hikari.Message positionally as their 2nd argument.

User context menus#
@tanjun.as_user_menu("Yeet user")
async def user_menu(
    ctx: tanjun.abc.MenuContext, user: hikari.InteractionMember
) -> None:
    # ... Yetting logic
    await ctx.respond(f"{user} got yeeted!", delete_after=30)

user menu example

And there's user context menus which will always take hikari.InteractionMember positionally as their 2nd argument.

These examples use delete_after to tell Tanjun to delete the response after 30 seconds if it still exists.

Multi-commands#

@tanchan.doc_parse.with_annotated_args(follow_wrapped=True)
@tanchan.doc_parse.as_slash_command()
@tanjun.as_message_command("meow command")
@tanjun.as_user_menu("meow command")
async def command(ctx: tanjun.abc.Context, user: annotations.User) -> None:
    """Do a thing to a user.

    Parameters
    ----------
    user
        The user to do the thing to.
    """
    # ... Thing logic
    await ctx.respond(f"Thing done to {user}")

When you want to use a command callback for multiple commands types there are a few things you'll have to note. The bases classes of tanjun.abc.Context or tanjun.abc.AppCommandContext (if this is only being used for slash and menu commands) should be used to type the first argument as these are base classes which only have cross-compatible features present (e.g. ephemeral responses are not implemented for the base Context as this isn't supported by message commands).

Command decorators like tanchan.doc_parse.with_annotated_args and the with_{}_check decorators present in tanjun.checks come with an optional follow_wrapped argument. Setting this to True will make it apply the decorator's change to every compatible command declaration in that decorator chain (rather than just the one it's directly on).

When using the same callback for a context menu and slash or message command you'll have to make sure the main option can be passed both positionally (as the 2nd argument) and by its name since context menus will still pass it positonally.

Checks#

Checks are callbacks which decide whether a command or group of commands should run for a command execution context. There's a collection of checks in tanjun.checks which can be applied to any command type.

These can be added to commands through decorator calls (e.g. @tanjun.with_guild_check) or by calling command.add_check(check).

Loading commands#

Tanjun doesn't let you load commands directly into its clients, instead you need to create a tanjun.Component instance and load the commands into it then load that into the client.

There's 2 different ways to load commands into a component:

component = tanjun.Component()

@component.with_command(follow_wrapped=True)
@tanjun.as_slash_command("name", "description")
@tanjun.as_message_command("name")
async def command(ctx: tanjun.abc.Context) -> None: ...

@tanjun.as_slash_command("name", "description")
async def other_command(ctx: tanjun.abc.Context) -> None: ...

component.add_command(other_command)

If you build the component first then you can use the Component.add_command and Component.with_command methods to add commands to the component directly. with_command supports follow_wrapped.

@tanjun.as_slash_command("name", "description")
async def slash_command(ctx: tanjun.abc.SlashContext) -> None: ...

component = tanjun.Component().load_from_scope()

Alternatively, you can call load_from_scope at the end of the module to load command objects (plus some other objects like schedules) into the component from the module's top level automatically.

Loading into clients#

component = tanjun.Component()

# ...

loaders = component.make_loader()

Component loaders give an easy way to load functionality and commands into a tanjun client from a module.

bot = hikari.GatewayBot("Token")
(
    tanjun.Client.from_gateway_bot(bot, declare_global_commands=True)
    .load_directory("./components", namespace="bot.components")
    .load_modules("bot.admin")
)

bot.run()

Normally you'd load modules straight into the client as shown in the example above. Client.load_modules loads from specific modules while Client.load_directory loads from the modules directly in a directory (no recursion). While load_modules takes python module paths (same paths you use in import syntax), load_directory takes a system path and the python module path for that directory as the keyword argument namespace. The namespace is appended onto python file names as f"{namespace}.{path.name[:3]}".

bot = hikari.GatewayBot("Token")
client = tanjun.Client.from_gateway_bot(bot)

(
    tanjun.dependencies.HotReloader()
    .add_directory("./components", namespace="bot.components")
    .add_modules("bot.admin")
    .add_to_client(client)
)

bot.run()

Tanjun also provides a standard hot reloader implementation which has a similar interface to Client for marking modules to track and will reload or unload target modules when it detects a that its file has been changed. tanjun.HotReloader will also redeclare application commands when they're changed unless redeclare_cmds_after=None is passed to its init.

Only one of these approaches should be used at the time since they'll conflict with each other.

See You Space Cowboy#

For more information on Tanjun's features (including features not covered here and other ways to declare commands) see its usage guide.