diff --git a/docs/plugins/commands.mdx b/docs/plugins/commands.mdx index bede9172044..a0871a94ed0 100644 --- a/docs/plugins/commands.mdx +++ b/docs/plugins/commands.mdx @@ -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 @@ -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 @@ -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", @@ -47,17 +48,15 @@ module.exports = definePlugin({ description: "Shout out a viewer", usage: "!so ", 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}`), }); ``` @@ -76,66 +75,56 @@ plugin.commands({ "description": "Shout out a viewer", "usage": "!so ", "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="!") ``` -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`. -The !help response in chat, listing the commands an enabled plugin provides with their descriptions, grouped by plugin +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. - - - -```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()), -}); -``` - - - - -```python -from owncast_plugin import plugin, define_commands, filter +The !help response in chat, listing the commands an enabled plugin provides with their descriptions, grouped by plugin -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. - - +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`). diff --git a/docs/plugins/events.mdx b/docs/plugins/events.mdx index 9254874570d..051891a6ae0 100644 --- a/docs/plugins/events.mdx +++ b/docs/plugins/events.mdx @@ -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. diff --git a/docs/plugins/sdks/javascript.md b/docs/plugins/sdks/javascript.md index 43dd6343091..4acdc907ee8 100644 --- a/docs/plugins/sdks/javascript.md +++ b/docs/plugins/sdks/javascript.md @@ -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`). diff --git a/docs/plugins/sdks/python.md b/docs/plugins/sdks/python.md index 60d84599e8e..3e877c9a6cc 100644 --- a/docs/plugins/sdks/python.md +++ b/docs/plugins/sdks/python.md @@ -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