Hooks

Hooks are a way to perform extra logic before and after a command is invoked. This is done using the concept of an ‘execution pipeline’ that has several steps which are executed in order when the user calls a command from within discord.

All the steps must complete successfully in order for a command invocation to be considered complete. If any one of the hooks fails for any of the steps, the pipeline fails and an error will be thrown.


Creating a Hook

Hooks are created using the lightbulb.hook decorator. They can be either synchronous or asynchronous functions - both will work. When creating a hook, you must specify the step that it runs on, and you can optionally specify whether the hook should be skipped if the pipeline has already failed.

By default, all hooks will always run no matter whether the pipeline has failed or not - the exception is the command invocation function, which will never be executed if any preceding hooks have failed. Pass skip_when_failed=True to the hook decorator to avoid execution of the hook if the pipeline has already failed.

To fail the pipeline, hooks can raise any exception. This will be caught and re-raised as an ExecutionPipelineFailedException once any remaining hooks have been run.

Below is an example hook that checks if the person who invoked the command is me (thomm.o) and will fail otherwise.

@lightbulb.hook(lightbulb.ExecutionSteps.CHECKS)
def fail_if_not_thommo(_: lightbulb.ExecutionPipeline, ctx: lightbulb.Context) -> None:
    if ctx.user.id != 215061635574792192:
        raise RuntimeError("only thomm.o can use this command")

As seen above, hooks must take at least 2 arguments - the execution pipeline being run, and the context that the command was invoked under. Lightbulb will attempt to dependency-inject any further arguments.


Adding Hooks to Commands

Adding hooks to commands is very simple - you just pass them to the hooks class parameter.

class YourCommand(
    lightbulb.SlashCommand,
    ...,
    hooks=[fail_if_not_thommo]
):
    ...

When the command is invoked, hooks will be executed in the order they are defined, grouped by execution step.


Step Order

Lightbulb provides a default step order - however, you can specify your own custom one when the Client is created which will cause command execution to follow your defined steps instead.

  • The default order is exported as lightbulb.DEFAULT_EXECUTION_STEP_ORDER if you wish to augment it when creating your own.

  • The provided execution steps are contained within lightbulb.ExecutionSteps if you wish to use them.

Warning

Custom orders must contain the step lightbulb.ExecutionStep.INVOKE otherwise the command invocation callback will never be run. Hooks also cannot be added for this step and will throw an error if you try to do so.


Custom Steps

Creating your own custom execution step is very simple and supported out-of-the-box by Lightbulb. All you have to do is create an instance of ExecutionStep with a custom ID. Note that all step IDs must be unique - you cannot define a new step with the same ID as an existing one.

YOUR_STEP = lightbulb.ExecutionStep("YOUR_STEP_ID")

Worked Custom Step Example

As a demo example for customizing the execution step order and defining a custom step, we are going to implement automatic deferral of command responses before any other steps are run.

import hikari
import lightbulb

# Define our custom execution step
AUTO_DEFER = lightbulb.ExecutionStep("AUTO_DEFER")

bot = hikari.GatewayBot(...)
client = lightbulb.client_from_app(
    bot,
    # Add our custom step to the step order. Our step will run first, followed by all the
    # default steps lightbulb would usually use.
    execution_step_order=[AUTO_DEFER, *lightbulb.DEFAULT_EXECUTION_STEP_ORDER]
)
bot.subscribe(hikari.StartingEvent, client.start)


# Define our hook to defer the command response
@lightbulb.hook(AUTO_DEFER)
async def auto_defer_command_response(_: lightbulb.ExecutionPipeline, ctx: lightbulb.Context) -> None:
    await ctx.defer()


@client.register
class AutoDeferredCommand(
    lightbulb.SlashCommand,
    name="auto-defer",
    description="auto defer test",
    # Add our hook to the command
    hooks=[auto_defer_command_response]
):
    @lightbulb.invoke
    async def invoke(self, ctx: lightbulb.Context) -> None:
        await ctx.respond("This command was auto-deferred!")


bot.run()

Hook Inheritance

Sometimes, you may find yourself frequently adding the same hooks to all your commands. Lightbulb allows you to pass some common hooks when creating your client which will then be applied to all commands registered to the client upon invocation.

For example, if you want all your commands to only be usable by yourself then you could do the following:

# Define the hook
@lightbulb.hook(lightbulb.ExecutionSteps.CHECKS)
async def only_me(_: lightbulb.ExecutionPipeline, ctx: lightbulb.Context) -> None:
    if ctx.user.id != YOUR_USER_ID:
        raise RuntimeError("you are not allowed to use the command")

# Add the hook to the client
client = lightbulb.client_from_app(..., hooks=[only_me])

# This command will only be usable by you!
@client.register
class YourCommand(...):
    ...

Built-in Hooks

Lightbulb provides a few hooks for common use-cases so that you don’t have to implement them yourself. They can be found in the lightbulb.prefab subpackage. See the api reference for details on what is available.

These include:

  • Checks

  • Cooldowns (using fixed-window or sliding-window algorithms)

  • Max concurrency (limiting the number of command invocations happening at once)