Bot Modules
Create custom bot modules for chat automation.
Overview
Bot modules are Rust structs implementing the BotModule trait. Each module can register commands, respond to events, and interact with the chat system.
Creating a Module
use lo_bot_modules::{BotModule, ModerationAction, ModuleConfig, ModuleResult};
use lo_chat::ChatMessageInput;
pub struct MyModule;
impl BotModule for MyModule {
fn name(&self) -> &str {
"my_module"
}
fn check_message(
&self,
msg: &ChatMessageInput,
_config: &ModuleConfig,
) -> Option<ModuleResult> {
if msg.message.contains("badword") {
Some(ModuleResult {
module_name: self.name().to_string(),
action: ModerationAction::Delete,
reason: "matched rule".into(),
})
} else {
None
}
}
}
Registration
Register your module with a ModuleRegistry (see crates/lo-bot-modules/src/registry.rs). The create_default_registry() helper pre-registers LinkProtection, WordFilter, SpamProtection, and TimedMessages. Modules are checked in the order they are registered — the first module to return Some(ModuleResult) wins. Modules are enabled/disabled per account via the bot_module_configs table (ModuleConfig.enabled).
Available Context
Modules have access to:
- Message content and metadata
- User information and roles
- Channel configuration
- Database queries via SQLx
- Redis cache
Extension Bot Modules
Extension bot modules extend the built-in module system with community-developed moderation and automation logic. Unlike built-in modules (which are Rust structs compiled into the bot binary), extension bot modules run as handler-based server functions inside V8 isolate sandboxes.
Architecture
Chat message / Platform event
│
├─ Built-in modules (Rust, in-process)
│ └─ LinkProtection, SpamProtection, WordFilter, TimedMessages
│
└─ Extension bot modules (V8 isolate, out-of-process)
│
Bot Module Worker
├─ Subscribes to Redis channel: bot_modules:{account_id}
├─ Loads active triggers from internal API
├─ Matches incoming messages/events against triggers
└─ Dispatches to V8 isolate handler
├─ Handler receives ctx (db, fetch, secrets, auth, user, cache, defer)
├─ Returns action: send_message | delete | timeout | ban | none
└─ Worker executes action via platform API
Bot Module Worker
The Bot Module Worker (apps/api/src/workers/bot_module_worker.rs) is a background service that bridges the chat event stream with extension bot module handlers. It runs inside the API server process alongside other workers (Token Refresh, EventSub, etc.).
Lifecycle:
- On startup, the Worker loads all active extension bot module triggers for connected accounts via
GET /v1/internal/bot-modules/{account_id}/triggers(SystemKey auth). - It subscribes to
bot_modules:{account_id}Redis channels for each active account. - When a chat message or platform event arrives, the Worker evaluates it against the loaded trigger set.
- On a match, the Worker dispatches the message to the corresponding V8 isolate handler via
crates/lo-extensions-v8/. - The handler's return value (action + optional message) is executed by the Worker using the account's channel connection credentials.
Trigger refresh: When an account installs, uninstalls, enables, or disables an extension bot module, a Redis pub/sub message on bot_module_triggers:{account_id} triggers the Worker to reload that account's trigger set.
Redis Channels
| Channel | Purpose |
|---|---|
bot_modules:{account_id} | Chat messages and events forwarded to extension bot modules |
bot_module_triggers:{account_id} | Trigger set invalidation (install/uninstall/enable/disable) |
V8 Execution
Extension bot module handlers execute inside the same V8 isolate sandbox used by all extension server functions (crates/lo-extensions-v8/). The handler context (ctx) includes:
| Property | Description |
|---|---|
ctx.db | Extension-scoped database access (ext_{extension_id}.* tables) |
ctx.fetch | HTTP client (egress allowlist enforced) |
ctx.secrets | Server-side secrets |
ctx.auth | Caller identity (userId, accountId) |
ctx.user | Chat user context (platform, platform_user_id, display_name, roles) |
ctx.cache | In-memory key-value cache scoped to the extension install (TTL-based) |
ctx.defer() | Schedule a follow-up action after the handler returns (e.g., delayed response) |
The same sandbox limits apply: 256 MB memory, 10s CPU timeout, no filesystem access, no dynamic code evaluation.
Trigger Matching Flow
1. Message arrives on bot_modules:{account_id} Redis channel
2. Worker iterates over active triggers for the account (ordered by priority)
3. For each trigger:
a. command → check if message starts with the configured prefix
b. keyword → check if message contains any configured keyword
c. pattern → match message against configured regex
d. event → check if the event type matches
e. timer → check if the interval has elapsed since last fire
f. moderate → always matches (runs on every message)
4. First matching trigger wins (like built-in modules)
5. Worker spawns V8 isolate, passes message + trigger config to handler
6. Handler returns { action, message?, duration? }
7. Worker executes the action via platform API
Priority order: moderate triggers run first (they are moderation gates), followed by command, keyword, pattern, event, and timer in registration order.
Key Files
| Path | Description |
|---|---|
apps/api/src/workers/bot_module_worker.rs | Bot Module Worker service |
crates/lo-extensions-v8/src/bot_module.rs | V8 isolate handler execution |
crates/lo-extensions/src/triggers.rs | Trigger types, matching, and serialization |
crates/lo-extensions/src/manifest.rs | Bot module manifest validation |
apps/api/src/rest/bot_modules.rs | REST endpoints (kill switch, trigger resolution) |
Security Headers
The Extension Supervisor and Runtime apps serve HTTP security headers via Cloudflare Pages _headers files:
- Supervisor (
apps/extension-supervisor/_headers):frame-srcrestricts which origins can be iframed (onlyext.lumio.visionand staging/prod preview).frame-ancestorsrestricts who can embed the Supervisor (onlylumio.visionand staging/prod preview). - Runtime (
apps/extension-runtime/_headers):worker-srcrestricts Worker origins.frame-ancestorsrestricts embedding to Supervisor origins only.connect-srcallows Sentry error reporting.
When adding a new environment domain, both _headers files must be updated alongside DNS and CF Pages configuration.
Config Validation
Extension config values are validated server-side against the extension's config_schema before writing to extension_installs.config. The validator (crates/lo-extensions/src/config_validator.rs) checks:
- Type match (string, number, boolean, color, select)
- Required fields present and non-null
- Select values in declared options
- Color hex format (#RRGGBB)
- Unknown keys rejected
Both GraphQL (configureExtension mutation) and REST (PATCH /extension-installs/\{id\}) enforce validation. The config is loaded from the database and passed to V8 handlers via ctx.config.