Modules - Commands
Commands are how chat users interact with modules. Someone types !something and the matching handler runs. The command system handles parsing, permission checks, cooldowns, and aliases. Modules just provide the handler.
@on_command Decorator
The basic decorator takes a command name and optional keyword arguments:
from owlbot.api import CommandContext, on_command
@on_command("hello")
async def hello(ctx: CommandContext) -> None:
await ctx.owncast_client.send_message(f"Hey there, {ctx.user.display_name}!")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
(required) | Primary command name (case-insensitive). |
aliases |
list[str] | tuple[str, ...] | None |
None |
Alternative names for the command. |
requires_authenticated |
bool |
False |
If True, the user must be authenticated. |
requires_moderator |
bool |
False |
If True, the user must have the MODERATOR scope. |
cooldown |
int | float |
0 |
Minimum seconds between invocations (0 to disable). |
A more complete example:
@on_command(
"ban",
aliases=["banuser"],
requires_moderator=True,
cooldown=5,
)
async def ban_user(ctx: CommandContext) -> None:
target = ctx.args.strip()
if not target:
await ctx.owncast_client.send_message("Usage: !ban <username>")
return
# ... ban logic
Parsing
When a chat message starts with the command prefix (default !), the message is parsed into a CommandEvent:
Given the message !addcommand !hello Hello world!:
| Property | Value |
|---|---|
ctx.prefix |
"!" |
ctx.command |
"addcommand" |
ctx.args |
"!hello Hello world!" |
ctx.args_list |
["!hello", "Hello", "world!"] |
The command property always returns the canonical command name, even if invoked via an alias. So if addcmd is an alias for addcommand, typing !addcmd !hello world still gives ctx.command == "addcommand".
Command matching is case-insensitive: !Hello, !HELLO, and !hello all match a command named "hello". If no matching command is found, the message is ignored and no error is raised.
Permissions
Two permission gates are available:
requires_authenticated=True: The user must be authenticated (user.is_authenticated).requires_moderator=True: The user must have the MODERATOR scope (user.is_moderator).
When a permission check fails, a private system message is sent to the user explaining why. The command handler is not called. No error is raised; other viewers see nothing happen.
Links to Protected Routes
If a command sends a user to one of your module's protected routes, use ctx.session_url_for(path) instead of ctx.routes.url_for(path). The generated URL is tied to the user who ran the command and connects their browser before redirecting to the route.
from owlbot.api import CommandContext, on_command
@on_command("settings", requires_moderator=True)
async def settings(ctx: CommandContext) -> None:
url = ctx.session_url_for("/settings")
await ctx.owncast_client.send_system_message_to_client(
ctx.chat_event.client_id,
f"Open settings: {url}",
)
Use plain ctx.routes.url_for(path) for public pages. Use ctx.session_url_for(path) for pages that require a browser session, authentication, or moderator access. See Protected Routes for route-level session and permission checks.
Cooldowns
Cooldowns are global per-command, not per-user. Setting cooldown=10 means the command can only fire once every 10 seconds regardless of who invokes it. This uses time.monotonic() internally so it's not affected by clock changes.
When a command is on cooldown, a private system message is sent to the user with the remaining wait time.
@on_command("status", cooldown=30)
async def server_status(ctx: CommandContext) -> None:
status = await ctx.owncast_client.get_status()
viewers = status.get("viewerCount", 0)
await ctx.owncast_client.send_message(f"Current viewers: {viewers}")
Dynamic Registration
Commands can be registered and unregistered at runtime through ctx.commands:
from owlbot.api import ModuleContext, CommandContext, on_setup
async def my_handler(ctx: CommandContext) -> None:
await ctx.owncast_client.send_message("Dynamic command!")
@on_setup
async def setup(ctx: ModuleContext) -> None:
ctx.commands.register(
name="dynamic",
handler=my_handler,
aliases=["dyn"],
)
Available Methods
| Method | Description |
|---|---|
ctx.commands.register(name, handler, ...) |
Register a new command. Accepts the same parameters as @on_command. |
ctx.commands.unregister(name) |
Remove a command by name. Only works for commands owned by the calling module. |
ctx.commands.get(name) |
Get a CommandInfo for a command by name or alias. Only returns commands owned by the calling module. |
ctx.commands.exists(name) |
Check if a command name or alias is registered by any module. |
ctx.commands.prefix |
Property: the command prefix character (e.g., "!"). |
ctx.commands.module_commands |
Property: dict of commands registered by this module only (maps name to CommandInfo). |
CommandInfo has these attributes:
| Attribute | Type | Description |
|---|---|---|
name |
str |
Canonical command name. |
handler |
CommandHandler |
The handler function. |
module_name |
str |
Module that owns this command. |
aliases |
frozenset[str] |
Set of alias names. |
requires_authenticated |
bool |
Authentication requirement. |
requires_moderator |
bool |
Moderator requirement. |
cooldown |
int | float |
Cooldown in seconds. |
all_triggers |
frozenset[str] |
The command name combined with all aliases. |
Relationship to Events
Commands build on top of the event system. For any chat message, CHAT event handlers run first in priority order. If no handler calls stop_propagation(), command dispatch runs afterward. This allows event handlers like spam filters or rate limiters to prevent a command from firing by calling ctx.stop_propagation().
Built-in Modules
Creating New Modules