Bot Modules
Overview
The Bot Modules system provides configurable chat bot modules for automated moderation and messaging. Each module can be independently enabled/disabled per account, with module-specific configuration and exemption rules. The four built-in modules are link protection, spam protection, word filter, and timed messages. The three moderation modules support moderator, VIP and subscriber exemptions; Timed Messages counts eligible chat lines and rotates a newline-separated message list.
Architecture
An installed module with event triggers has an On test events setting in All settings. Its reserved config.test_events value is prefix (default) or ignore. A test reaches an event() handler under prefix; its reply and chat:send actions start with [Test] once. Under ignore, the handler does not run. The worker never performs chat:ban, chat:unban, chat:timeout or chat:delete on a test. It performs overlay:update_layer, obs:*, spotify:* and events:emit only when the test has raw.showOnStream: true; other actions still run and can inspect event.raw.isTest. Only installs with event triggers and events:read show the setting. The install's whole config is replaced when it is configured, so API clients must preserve other keys when writing test_events.
Backend
- GraphQL (
apps/api/src/graphql/bot_modules.rs) and REST (apps/api/src/routes/bot_modules.rs) -- list and get modules, switch a module on or off, change its settings, reset it. - Service (
apps/api/src/services/bot_modules.rs) -- the one write path GraphQL, REST and the MCP server share: plan gate, name check, write. - Store -- each built-in module is a system extension installed for every account (
system-link-protection,system-spam-protection,system-word-filter,system-timed-messages). On/off and the settings live in that install (extension_installs.enabled,extension_installs.config), which is exactly what the bots and the Bot Module Worker run. Thebot_module_configsrow (apps/api/src/db/bot_modules.rs) is the module list's entry and holdsexempt_roles; its ownenabled/configare a copy kept in step, and every read returns the install's values instead. - Validation -- Module names are validated against a whitelist:
link_protection,spam_protection,word_filter,timed_messages.
Frontend
- Bot modules settings page with a card per module.
- Toggle switch for enable/disable.
- Module-specific configuration form.
- Exempt roles selector (checkboxes for mods, VIPs, subs).
API
GraphQL Queries
| Query | Permission | Description |
|---|---|---|
botModules | bot-modules:read | List all bot module configs for the account |
botModule(name) | bot-modules:read | Get a single bot module config by name. Validates name against whitelist. |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
updateBotModule(name, enabled, config?, exemptRoles?) | bot-modules:edit | Switch a module on or off. With config, also replaces its settings (validated against the module's config_schema); without config the settings stay. exemptRoles defaults to {}. |
deleteBotModule(name) | bot-modules:edit | Reset a module to its default settings. On/off stays as it was. Returns whether anything was reset. |
REST Endpoints
All paths live under /v1.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/bot-modules | bot-modules:read | List all module configs for the account |
PUT | /v1/bot-modules/{module_name} | bot-modules:edit | Body { enabled, config?, exempt_roles? }. Same behaviour as updateBotModule: omitted config keeps the settings; exempt_roles defaults to mods only. |
DELETE | /v1/bot-modules/{module_name} | bot-modules:edit | Reset the module to its default settings (204); on/off stays |
Both protocols return the same errors: the module is not on the plan, the name is not a valid module, or Config validation failed: … (REST 400) when config does not match the module's config_schema. Nothing is written in any of these cases. When the account has uninstalled the system extension behind a module, only the module list's row changes; nothing runs for that module until it is installed again.
Valid Module Names
| Module | Description |
|---|---|
link_protection | Detects and moderates messages containing links |
spam_protection | Detects and moderates spam patterns (repeated messages, excessive caps, etc.) |
word_filter | Filters messages containing blocked words/phrases |
timed_messages | Sends automatic messages at configured intervals |
Permissions
| Permission | Description |
|---|---|
bot-modules:read | View bot module configurations |
bot-modules:edit | Create, update, and delete bot module configurations |
Database
| Table | Database | Description |
|---|---|---|
extension_installs | PostgreSQL | The account's install of the module's system extension. enabled and config are the module's on/off state and settings — the store the runtime reads |
bot_module_configs | PostgreSQL | The module list's row: id, account_id, module_name, extension_id, install_id, exempt_roles (JSONB), created_at, updated_at. enabled/config are a copy of the install's values |
Config JSONB Structure (per module)
The config field contains module-specific settings. Examples:
link_protection:
{
"action": "delete",
"warnMessage": "Links are not allowed in chat.",
"whitelistDomains": "youtube.com, twitch.tv",
"blacklistDomains": "malware.com",
"timeoutDuration": 60,
"exemptMods": true
}
Whitelist and blacklist entries are comma-separated. They match a domain itself and its subdomains, on a dot boundary: youtube.com covers youtube.com and m.youtube.com, but not evilyoutube.com or youtube.com.evil.net. Entries are compared case-insensitively, and a trailing dot is ignored. A blacklisted domain is moderated; when a whitelist is set, a domain outside it is moderated too. With neither list, all links are moderated.
spam_protection:
{
"maxCapsPercent": 80,
"minCapsLength": 8,
"maxEmotes": 15,
"maxSymbolsPercent": 50,
"action": "timeout",
"timeoutDuration": 30
}
The module checks excessive capitals, symbols and emote-like patterns; it does not count repeated messages. The symbol check counts letters, digits and combining marks of every script as text, so umlauts, Cyrillic, CJK or Devanagari messages are not treated as symbol spam; punctuation and emoji count as symbols.
word_filter:
{
"bannedWords": "badword1, badword2",
"bannedPatterns": "regex_pattern",
"useWildcards": false,
"action": "delete",
"warnMessage": "Your message contained a banned word."
}
Banned words are separated by commas or line breaks. Banned patterns are separated by line breaks, or by commas outside (...), [...] and {...}: a quantifier like a{2,5} or a class like [,;] stays one pattern. Write \, for a literal comma at the top level of a pattern.
timed_messages:
{
"messages": "Follow us on Twitch!\nJoin our Discord!",
"intervalSeconds": 300,
"onlyWhenLive": true,
"minChatLines": 5
}
Min Chat Lines Between Messages counts every chat message the module sees (broadcaster lines and the module's own messages are not counted). The next timed message is posted only once the count is reached, and the count starts over after each post. The messages rotate at one shared interval, measured in seconds. To count, the module declares chat:read and runs as a moderation handler that always allows.
Exempt Roles JSONB Structure
{
"moderators": true,
"vips": true,
"subscribers": false
}
Data Flow
- Account owner/admin enables a bot module and configures it via the dashboard.
- The switch and the settings are saved with
updateBotModule/PUT /v1/bot-modules/{module_name}into the module's system install; the bots and the Bot Module Worker reload it right away. - When a chat message is received, the bot checks each enabled module:
- Is the user exempt (mod/VIP/sub based on the module's exempt settings)? Broadcaster messages are never moderated; moderator messages are checked unless Exempt Moderators is on.
- Does the message violate the module's rules (based on
config)?
- If a violation is detected, the configured action (delete, timeout, warn) is executed.
- Timed Messages sends one message from its rotating list at each configured interval when its chat-line minimum is met and, if Only When Live is enabled, at least one chat platform is live.
Feature gating
The whole Bot Modules surface is gated on the umbrella feature:bot_modules
flag: every query and mutation (botModules, botModule, updateBotModule,
deleteBotModule on GraphQL; GET/PUT/DELETE /v1/bot-modules on REST)
enforces it on both protocols - GraphQL via
FeatureGuard::new("feature:bot_modules") chained before the permission guard,
REST via require_feature(..., "feature:bot_modules", ...) after the permission
check. With feature:bot_modules disabled the API rejects the call directly
(Feature 'feature:bot_modules' is not available, HTTP 403), so listing
configs is gated too - the webapp route gate is UX only, not the boundary.
In addition, each built-in module has its own finer feature flag, keyed
bot_module:{module_name} - bot_module:link_protection,
bot_module:spam_protection, bot_module:word_filter,
bot_module:timed_messages. Writing or deleting a module config checks that
per-module flag on both protocols after the umbrella feature:bot_modules
check, so a specific module disabled for the account's plan cannot be
configured even when the Bot Modules area itself is enabled. Which plans enable
which module flag is a plan_features mapping, editable by admins without a
deploy.
Key Files
| Path | Description |
|---|---|
apps/api/src/graphql/bot_modules.rs | GraphQL queries and mutations |
apps/api/src/routes/bot_modules.rs | REST endpoints |
apps/api/src/services/bot_modules.rs | Update and reset shared by GraphQL and REST: per-module plan gate (bot_module:{name}), module-name check, write |
apps/api/src/db/bot_modules.rs | Database operations |
Extension Bot Modules
In addition to the four built-in modules above, accounts can install extension bot modules from the Lumio Extension Store. Extension bot modules are community-developed moderation and automation modules that run inside the same V8 isolate sandbox used by all extensions, with the same security guarantees.
How It Works
Extension bot modules use the bot_module category in lumio.config.json. When installed, the extension registers one or more triggers (commands, keywords, patterns, events, timers, or a moderation hook) that the bots and the Bot Module Worker run against incoming chat messages, stream events and the clock.
Chat message arrives at the platform bot (Twitch, Kick, YouTube, Discord)
→ Bot matches it against the account's extension triggers (command, keyword, regex)
→ Command: min_role + cooldown check, then the Bot Module Worker runs the handler
and the bot posts the reply
→ Keyword / pattern: published to the Bot Module Worker, which runs the handler
and sends the reply back through the bot
→ Moderation (Twitch, Kick, YouTube): the module's moderate handler decides
allow / delete / timeout / ban / warn within 500 ms
Stream event (follow, sub, raid, ...) → the Bot Module Worker runs the event handlers
Timer interval elapses → the Bot Module Worker runs the timer handler
→ Handlers run in the V8 isolate sandbox
→ Any actions a handler returns run after its reply, through the same
permission checks as widget actions
Installing, uninstalling or reconfiguring a bot module - or an admin suspending it - updates the bots' trigger set and the Bot Module Worker's install registry right away; no restart is needed. The same holds for the default system modules installed with a new account. Technical detail: Bot Modules (developer guide).
Installation and Configuration
Users install extension bot modules from the Extension Store like any other extension. Once installed, the module appears in Dashboard > Bot Modules alongside built-in modules. Each extension bot module supports:
- Enable/disable toggle per account
- Configuration via
config_schemafields defined by the extension developer (rendered as a form in the dashboard) - Trigger overrides - account owners can customize command prefixes, keyword lists, and regex patterns without modifying the extension code
- Exempt roles - the same moderator/VIP/subscriber exemption system used by built-in modules
Configuration Validation
Config values set by users (through the dashboard settings panel or API) are validated against the extension's declared schema. Invalid values (wrong type, missing required fields, unknown settings) are rejected with clear error messages. This ensures extensions always receive correctly typed configuration.
Kill Switch
Account owners can immediately stop all extension bot modules of their account with the kill switch. The kill switch:
- Stops extension triggers for the account instantly (no restart required) - commands, keywords, patterns, moderation, event handlers, timers and any pending actions
- Is stored per account, so a restarted bot starts with the switch still set, and stays in force when the bot reloads the account's triggers (an install change, a channel the bot joins later, a bot failover)
- Preserves every install's configuration, so turning it off resumes the modules as they were
- Also refuses every handler server function (
query()/mutation()/action()) of the account's extensions with403 EXTENSIONS_KILLED, since those run on the same worker; declarative server functions (queryRows,insertRow, …) keep working
It is set with POST /v1/bot-modules/kill ({ "account_id", "killed" }) or the GraphQL mutation setBotModuleKillSwitch(accountId, killed). Both require bot-modules:edit, only act on the caller's own account and write the audit event account:extension_kill_switch_set. To stop a single module, disable its install instead.
Disabled installs and installs of an extension that Lumio has suspended never reach the bots: the trigger feed leaves them out, and the Bot Module Worker drops their handlers on its next sync.
Trigger Types
Triggers are declared, not registered in code: the extension's
lumio.config.json carries a triggers object whose keys are the trigger
types below. Each entry names the handler the Bot Module Worker dispatches to.
triggers key | Description | Limits |
|---|---|---|
commands | Matches a chat command (e.g. !rank). Each entry carries name, description, cooldown_global, cooldown_user, min_role, and optional discord_options. | max 20 |
keywords | Matches any of a list of keywords in message text | max 50, min 3 chars each |
patterns | Matches a regex pattern against message text | max 10, max 200 chars each |
events | Fires on a stream event (e.g. twitch:follower); the module's event() handler runs | - |
timers | Fires on a recurring interval - name, interval (seconds), handler. The older form timer: true runs one timer named timer at the install's intervalSeconds setting (default 300 s, clamped to 60 s - 24 h) | min interval 60 s; shorter entries are ignored |
Read permissions. Triggers that hand the module chat or stream data run only
when the installed version's manifest declares the matching permissions entry:
keywords, patterns and moderation need chat:read, events need
events:read. Without it the API leaves those triggers out of both the bots'
trigger feed and the Bot Module Worker's handler feed, and the Worker refuses
keyword, pattern and moderate runs for the install on its own as well (fail
closed). Commands are addressed to the module by name and timers carry no chat
data, so they need neither. The built-in moderation modules (link protection,
spam protection, word filter) declare chat:read and the actions their
decisions run (chat:delete, chat:timeout, chat:ban, chat:send); timed
messages declares chat:read (to count chat lines) and chat:send.
Built-in module updates. On startup, once a system extension's new version
has its bundle stored, every install of it that follows updates
(auto_update on, no pinned version) moves to that version, with its manifest
and code. An install pinned to a version, or with auto-update off, stays where
it is; for those the seed adds the read permissions the current
lumio.config.json declares to the stored manifests of earlier versions (only
chat:read / events:read, never action permissions), so they keep their
triggers.
min_role accepts everyone, follower, subscriber, vip, moderator, or
broadcaster. Command triggers are access-controlled exactly like dynamic
commands: before a matched command reaches the Bot Module Worker, the bot checks
the sender's role against min_role and takes the cooldown_global /
cooldown_user slots (shared gate lo_bot_commands::access; Redis keys
lumio:cmd_cd:{account_id}:ext:{install_id}:{name}:…). A sender below min_role,
or a command still cooling down, is ignored silently. The worker re-checks
min_role before it runs a command handler and refuses a handler whose minimum it
does not know. The effective values are the manifest defaults with the install's
config.triggers.commands[] overrides on top. The command fields are snake_case as
documented (min_role, cooldown_global, cooldown_user); the camelCase spelling
of older CLI templates (minRole, …) is still read.
Events
A module that lists event types in triggers.events and declares
events:read has its event() handler run once for every matching stream event of the account (followers, subs,
raids, donations, …). The handler receives the same event payload widgets
receive. A reply goes to the chat of the platform the event came from
(Twitch, Kick or YouTube); Discord events have no chat channel, so a reply to
them is dropped.
Viewer input
Viewers interact with an extension only through chat commands (!vote 2 runs
the module's command("vote", …) handler) and channel point rewards on Twitch
and Kick (twitch:reward / kick:reward run its event() handlers). There is
no viewer page and no viewer identity. A handler can store the input with
ctx.db and push it with ctx.realtime.push(topic, payload) to the
extension's editor panel and overlay layer, which receive it with
useRealtime(topic, cb) over the WebSocket channel
extension_install:{install_id}. Event handlers run at most 50 times per
install and minute (shared with timers). Details:
Bot Modules (developer guide).
Timers
Each declared timer runs once per interval, no matter how many Bot Module Worker replicas are running. A missed interval (for example during a restart) is skipped, not caught up. Changing a timer's interval, disabling the install or setting the kill switch takes effect right away. A timer's reply goes to the chat of every platform the install runs on, except Discord.
Only while live. An install whose configuration sets onlyWhenLive: true
(the Timed Messages module offers this setting) runs its timer only while
at least one of its chat platforms is live, and posts only on the platforms
that are live.
Moderation
A module that exports a moderate handler and declares triggers.moderation: true and the
chat:read permission sees
every chat message of the account on Twitch, Kick and YouTube and returns a
decision:
| Decision | What happens | Permission the module must declare |
|---|---|---|
allow | nothing | - |
delete | the message is deleted | chat:delete |
timeout | the sender is timed out for the decision's duration (default 600 s) | chat:timeout |
ban | the sender is banned | chat:ban |
warn | the decision's message (or its reason) is posted to chat | chat:send |
Moderation never slows chat down: each check has 500 ms, and a check that
times out, fails or arrives while the account already has 16 checks running
lets the message through. Messages from the broadcaster are not moderated.
Moderator messages reach the handler with user.isMod set, so the module
decides (the built-in modules skip them while Exempt Moderators is on, the
default). A warn whose text equals the message it answers is not posted, so a
bot that sees its own posts cannot warn itself in a loop. On Discord moderate handlers are not called, because none of
the chat actions supports Discord.
Actions
Besides a chat reply, a handler can return actions - the same actions a
widget can run (for example chat:timeout, obs:set_scene, spotify:skip).
They run after the reply, at most once each (a failed action is not retried),
and pass the same checks as widget actions:
- the module's manifest must declare the action name as a permission;
- the account's plan must include the action's feature (
chat:*→ multichat,obs:*→ OBS remote,spotify:*→ music); - the parameters must be valid.
Each install may run at most 120 actions per minute (operator setting
[extensions] bot_module_action_rate_per_minute); actions beyond that are
refused until the next minute. Every outcome - success, refusal, failure,
including moderation decisions - appears in the install's logs in the
dashboard, at most one entry per second; an allow decision writes nothing.
Log entries carry the action name and, for chat actions, the platform and
target ids - never message text.
Bans, unbans and timeouts a module issues are recorded in the account's
audit log as account:extension_moderation with source
bot_module. Message deletions by a module appear only in the install log.
Permissions
Extension bot modules use the same permission system as other extensions: an action a handler returns runs only when the manifest declares it (see Actions). The chat:ban permission requires enhanced review during the extension approval process due to its impact on user experience.
Feature gating
Extension bot modules are gated by the extension-platform feature flags rather than by a plan constant. Installing and running them requires the account to have the extension platform enabled; see Extensions.
Key Files
| Path | Description |
|---|---|
apps/bot-module-worker/ | Bot Module Worker HTTP service (binary) |
crates/lo-bot-module-worker/src/router.rs | Trigger matching and dispatch logic |
crates/lo-bot-module-worker/src/executor.rs | Handler execution and result handling |
crates/lo-bot-module-worker/src/types.rs | InstallTriggerConfig, CommandTrigger, KeywordTrigger, PatternTrigger |
crates/lo-extensions-v8/src/executor.rs | V8 isolate execution for extension handlers |
apps/api/src/routes/bot_module_extensions.rs | Trigger sync, kill switch, and extension log/metric endpoints |