Skip to main content

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:

  1. 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).
  2. It subscribes to bot_modules:{account_id} Redis channels for each active account.
  3. When a chat message or platform event arrives, the Worker evaluates it against the loaded trigger set.
  4. On a match, the Worker dispatches the message to the corresponding V8 isolate handler via crates/lo-extensions-v8/.
  5. 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

ChannelPurpose
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:

PropertyDescription
ctx.dbExtension-scoped database access (ext_{extension_id}.* tables)
ctx.fetchHTTP client (egress allowlist enforced)
ctx.secretsServer-side secrets
ctx.authCaller identity (userId, accountId)
ctx.userChat user context (platform, platform_user_id, display_name, roles)
ctx.cacheIn-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

PathDescription
apps/api/src/workers/bot_module_worker.rsBot Module Worker service
crates/lo-extensions-v8/src/bot_module.rsV8 isolate handler execution
crates/lo-extensions/src/triggers.rsTrigger types, matching, and serialization
crates/lo-extensions/src/manifest.rsBot module manifest validation
apps/api/src/rest/bot_modules.rsREST 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-src restricts which origins can be iframed (only ext.lumio.vision and staging/prod preview). frame-ancestors restricts who can embed the Supervisor (only lumio.vision and staging/prod preview).
  • Runtime (apps/extension-runtime/_headers): worker-src restricts Worker origins. frame-ancestors restricts embedding to Supervisor origins only. connect-src allows 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.