Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 42 additions & 53 deletions docs/plugins/commands.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Chat commands
description: Register a chat command table, get an automatic !help, and compose commands with your own chat handlers and filters.
description: Declare chat commands with aliases, cooldowns, moderator gating, and automatic !help listings.
sidebar_position: 6
sidebar_label: Commands
toc_min_heading_level: 2
Expand All @@ -15,20 +15,21 @@ tags:
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Plugins that respond to chat commands like `!uptime` or `!so` can declare a command table instead of hand-rolling prefix parsing, aliases, moderator gates, and cooldown tracking. The host routes the commands for you and answers `!help` automatically.

Commands build on the [chat basics](/docs/plugins/chat). That page covers the chat message payload, sending messages, and filters. This page is just the command layer on top.
Declare a command table for commands such as `!uptime` or `!so`. Command tables
support aliases, moderator gates, per-user cooldowns, parsed arguments, and
automatic `!help` listings. See [chat basics](/docs/plugins/chat) for the chat
message payload, sending messages, and filters.

## Register a command table

A command table gives you:
A command table provides:

* a configurable command prefix (default `!`)
* a configurable command prefix, with `!` as the default
* per-command `description`, `usage`, and `aliases`
* per-user cooldowns, clocked off `msg.timestamp`
* per-user cooldowns
* moderator-only gating based on the sender's scopes
* an automatic, host-owned `!help`
* a fallback for unrecognized commands.
* argument parsing
* an automatic `!help` listing

<Tabs groupId="plugin-lang">
<TabItem value="js" label="JavaScript" default>
Expand All @@ -37,7 +38,7 @@ A command table gives you:
const { definePlugin } = require("@owncast/plugin-sdk");

module.exports = definePlugin({
commandPrefix: "!", // optional, default "!"
commandPrefix: "!", // optional
commands: {
uptime: {
description: "How long we've been live",
Expand All @@ -47,17 +48,15 @@ module.exports = definePlugin({
description: "Shout out a viewer",
usage: "!so <name>",
aliases: ["shoutout"],
cooldownMs: 10_000, // per user, clocked off msg.timestamp
cooldownMs: 10_000,
run: (ctx) => ctx.reply(`go follow ${ctx.args[0] || "someone cool"}`),
},
clear: {
description: "Clear the chat",
modOnly: true, // requires the sender's scopes to include "MODERATOR"
modOnly: true,
run: (ctx) => ctx.replyPrivately("done"),
onDenied: (ctx) => ctx.replyPrivately("mods only"),
},
},
onUnknownCommand: (ctx) => ctx.replyPrivately(`unknown command: ${ctx.command}`),
});
```

Expand All @@ -76,66 +75,56 @@ plugin.commands({
"description": "Shout out a viewer",
"usage": "!so <name>",
"aliases": ["shoutout"],
"cooldown_ms": 10_000, # per user, clocked off msg.timestamp
"run": lambda ctx: ctx.reply(f"go follow {ctx.args[0] if ctx.args else 'someone cool'}"),
"cooldown_ms": 10_000,
"run": lambda ctx: ctx.reply(
f"go follow {ctx.args[0] if ctx.args else 'someone cool'}"
),
},
"clear": {
"description": "Clear the chat",
"mod_only": True, # requires the sender's scopes to include "MODERATOR"
"mod_only": True,
"run": lambda ctx: ctx.reply_privately("done"),
"on_denied": lambda ctx: ctx.reply_privately("mods only"),
},
}, prefix="!") # prefix optional, default "!"
}, prefix="!")
```

</TabItem>
</Tabs>

Each command handler receives a context with `msg`, `user`, `command`, `args`, `argString`, a public `reply`, and a private `replyPrivately` (whisper). `args` and `reply` are the same in both SDKs, and `argString`/`replyPrivately` are `arg_string`/`reply_privately` in Python. Gating uses the stable sender identity (`user.scopes`, `user.id`), so it's reliable rather than a display-name guess. The command table reports a message as recognized even when it was denied by cooldown or moderator gating.

## `!help` is automatic

The host owns `!help`. Type it in chat and the host lists every command's `description` across all enabled plugins, posted as a system message. You don't implement it, and it works even if your plugin holds no `chat.send` permission. Moderator-only commands stay hidden from non-moderators. The only thing you do to take part is declare a command table with descriptions.
JavaScript handlers receive `msg`, `user`, `command`, `invokedAs`, `args`,
`argString`, `reply`, and `replyPrivately`. Python uses `invoked_as`,
`arg_string`, and `reply_privately`.

<img src="/docs/img/plugin-help-command.png" alt="The !help response in chat, listing the commands an enabled plugin provides with their descriptions, grouped by plugin" width="320" />
Duplicate command names are allowed, so every matching plugin runs.
Unknown commands, moderator-gated invocations, and cooldown-limited invocations
are silent. Declaring or receiving a command requires no permission. Actions
inside the handler still require their usual permissions, such as `chat.send`.

## Using your own chat handlers instead
## `!help` is automatic but not reserved

A command table is optional. You can always read chat yourself in an `onChatMessage` handler (see [Chat plugins](/docs/plugins/chat)) and act on whatever you want. For a single fixed command that is often enough, though it won't appear in `!help` unless you declare a command table.
Owncast posts a system message listing command descriptions from every enabled
plugin. Moderator-only commands stay hidden from non-moderators. This built-in
response needs no plugin permission.

You can also combine the two. When you want to compose, for example to drop command invocations from public chat with a filter, use the lower-level router (`defineCommands` in JavaScript, `define_commands` in Python). It returns a callable you feed messages yourself, so you decide what happens around it:
`!help` and its `!commands` alias remain ordinary chat messages. Plugins may
also declare or respond to them. The built-in response does not block additional
plugin responses.

<Tabs groupId="plugin-lang">
<TabItem value="js" label="JavaScript" default>

```js
const { definePlugin, defineCommands, filter } = require("@owncast/plugin-sdk");
const commands = defineCommands({ commands: { /* same shape as above */ } });

module.exports = definePlugin({
filterChatMessage: (msg) => (commands(msg) ? filter.drop("command") : filter.pass()),
});
```

</TabItem>
<TabItem value="py" label="Python">

```python
from owncast_plugin import plugin, define_commands, filter
<img src="/docs/img/plugin-help-command.png" alt="The !help response in chat, listing the commands an enabled plugin provides with their descriptions, grouped by plugin" width="320" />

commands = define_commands({"commands": { }}) # same shape as above
## Using ordinary chat handlers and filters

@plugin.filter_chat_message
def hide_commands(msg):
return filter.drop("command") if commands(msg) else filter.pass_()
```
A command table is optional. A plugin may inspect `msg.body` in an
`onChatMessage` handler for a single fixed command. Hand-rolled commands do not
appear in the built-in help listing.

</TabItem>
</Tabs>
Command messages remain ordinary chat messages, so a plugin may use both a
command table and an ordinary chat handler. Chat filters run before command
matching. A message dropped by a filter does not execute any declared command.

## Example plugins

- **mod-commands** ([JavaScript](https://github.com/owncast/plugin-sdk/tree/main/examples/js/mod-commands), [Python](https://github.com/owncast/plugin-sdk/tree/main/examples/python/mod-commands)): a custom `?` prefix and a moderator-only command with on-denied gating.
- **mod-commands** ([JavaScript](https://github.com/owncast/plugin-sdk/tree/main/examples/js/mod-commands), [Python](https://github.com/owncast/plugin-sdk/tree/main/examples/python/mod-commands)): a custom `?` prefix with aliases, cooldowns, and moderator gating.
- **stream-tracker** ([JavaScript](https://github.com/owncast/plugin-sdk/tree/main/examples/js/stream-tracker), [Python](https://github.com/owncast/plugin-sdk/tree/main/examples/python/stream-tracker)): a command table with `!uptime` and `!who`, plus stream and chat-user activity tracking.
- **stream-ops** ([JavaScript](https://github.com/owncast/plugin-sdk/tree/main/examples/js/stream-ops), [Python](https://github.com/owncast/plugin-sdk/tree/main/examples/python/stream-ops)): a command table that reports broadcast and video telemetry.
- **timer-bot** ([JavaScript](https://github.com/owncast/plugin-sdk/tree/main/examples/js/timer-bot), [Python](https://github.com/owncast/plugin-sdk/tree/main/examples/python/timer-bot)): a command table for reminders and countdowns (`!remind`, `!every`, `!countdown`).
Expand Down
4 changes: 2 additions & 2 deletions docs/plugins/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,9 @@ Each filter can declare a priority. Lower numbers run earlier (default `100`). U
* Filters are time-capped at 50 ms. A slow filter is cancelled and treated as pass.
* After 5 consecutive failures (errors or timeouts) the plugin is auto-disabled for the rest of the session, with a one-time log line. A successful filter call resets the counter, so transient flakiness doesn't accumulate. Restart the host to re-enable.

### Command routing
### Command tables

Rather than hand-rolling prefix parsing, aliases, cooldowns, and moderator gating in your chat handler, declare a **command table**: the SDK wires the chat subscription and prefix parsing for you, and the host's built-in `!help` lists every command. Gating uses the sender identity on the message (`user.scopes`, `user.id`), not a display-name guess.
Declare a **command table** for aliases, cooldowns, moderator gating, parsed arguments, and automatic `!help` listings. Gating uses the sender identity (`user.scopes`, `user.id`), not a display-name guess.

<Tabs groupId="plugin-lang">
<TabItem value="js" label="JavaScript" default>
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/sdks/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The [`page-content-demo`](https://github.com/owncast/plugin-sdk/tree/main/exampl

## What's in the package

- `index.js`: the runtime: `definePlugin`, the `owncast.*` host wrappers, the `filter` constructor, `defineCommands`.
- `index.js`: the runtime with `definePlugin`, command handlers, the `owncast.*` host wrappers, and filter helpers.
- `index.d.ts`: TypeScript declarations for every event payload and host API.
- `testing.js`: the `runScenarios` / `runScenarioFiles` test API.
- `bin/owncast-plugin`: the CLI (`build`, `test`, `serve`, `package`).
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/sdks/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Payloads are attribute objects with `snake_case` accessors over the wire JSON (`
Two more Python idioms worth knowing, both documented in full (with Python examples) on the subject pages:

- **HTTP routing**: plugins with `http.serve` declare routes with decorators: `@plugin.get/post/put/delete/patch(path)`, `@plugin.route(path, methods=[...])`, `@plugin.on_http_request(path)`, and a bare `@plugin.on_http_request` catch-all. A handler returns a `dict` (`{status, body, headers}`), a `str` (→ 200), or `None` (→ 204). See [Serving HTTP](/docs/plugins/http).
- **Chat commands**: `plugin.commands({...})` declares a command table (the host's `!help` lists it automatically), with the lower-level `define_commands(...)` router underneath. See [Chat commands](/docs/plugins/commands).
- **Chat commands**: `plugin.commands({...})` declares commands with aliases, moderator gating, and per-user cooldowns. The built-in `!help` lists them automatically. See [Chat commands](/docs/plugins/commands).

## The CLI

Expand Down