Collects messages from chat connectors, parses commands, and emits command executions — the central nervous system of eevee.
The router is the core message-routing module in the eevee ecosystem. It sits between connectors (which receive messages from chat platforms like IRC and Discord) and command modules (which handle specific bot commands like !weather or !dice). When a chat message arrives, the router decides whether it matches a registered command or broadcast, enforces rate limits, and dispatches the message to the appropriate handler.
Without the router, every eevee module would need to listen to all chat traffic and do its own command matching. The router centralizes that responsibility: modules register their command patterns, and the router handles the rest — matching, filtering, blocking, rate limiting, and delivery.
The router communicates entirely over NATS, making it language-agnostic and deployable independently. It exposes an HTTP metrics endpoint for Prometheus scraping and responds to admin and stats requests over NATS.
- Command routing — Modules register command regex patterns; the router matches incoming messages and publishes to
command.execute.<uuid>subjects - Broadcast routing — Modules register broadcast filters; the router forwards matching messages to
broadcast.message.<uuid>subjects - Prefix handling — Supports both platform-specific prefixes (e.g.,
!) and nick-based addressing (e.g.,eevee: weather) - Configurable blocklist — Drop messages matching regex patterns, scoped by platform, network, instance, channel, or user
- Rate limiting — Per-command rate limits with configurable level (global, platform, instance, channel, user), interval, and overflow behavior (drop or enqueue)
- Admin API — NATS-based admin endpoints for inspecting the command registry and rate-limit statistics
- Prometheus metrics — Counters for messages, commands, broadcasts, registrations, rate limits, and processing times
- Stats reporting — Responds to
stats.emit.requestandstats.uptimewith module health data - Graceful shutdown — Drains NATS connections and cleans up registries on SIGINT/SIGTERM
This module is part of the eevee ecosystem and is not published independently. Install via the workspace:
cd /path/to/eevee/router
npm install| Variable | Required | Default | Description |
|---|---|---|---|
NATS_HOST |
Yes | — | NATS server hostname (e.g. nats://localhost:4222) |
NATS_TOKEN |
Yes | — | NATS authentication token |
MODULE_CONFIG_PATH |
Yes | — | Path to the router YAML config file |
HTTP_API_PORT |
No | 9000 |
Port for the Prometheus metrics HTTP server |
Specified via MODULE_CONFIG_PATH. The config file controls the message blocklist.
blocklist:
# Block messages matching a pattern on all platforms
- pattern: ".*spam.*"
enabled: true
description: "Block messages containing the word spam"
platform: ".*"
# Block only on a specific platform
- pattern: ".*buy now.*"
enabled: true
description: "Block 'buy now' phrases on IRC"
platform: "irc"
# Target a specific network and channel
- pattern: ".*test spam.*"
enabled: true
description: "Block test spam in a specific IRC channel"
platform: "irc"
network: "libera"
channel: "#bots"
# Ignore all messages from a specific user
- pattern: ".*"
enabled: true
description: "Ignore all messages from a specific Discord user"
platform: "discord"
user: "annoying-user.*"
# Disabled entries are skipped
# - pattern: ".*test.*"
# enabled: false
# description: "Disabled blocklist entry"
# platform: ".*"| Field | Required | Description |
|---|---|---|
pattern |
Yes | Regex matched against the message text |
enabled |
No | Set to false to disable the entry (defaults to true) |
description |
No | Human-readable description |
platform |
No | Regex matched against the message platform (e.g. irc, discord) |
network |
No | Regex matched against the network name |
instance |
No | Regex matched against the instance identifier |
channel |
No | Regex matched against the channel name |
user |
No | Regex matched against the user identifier |
All regex fields are matched case-sensitively. Omitted scope fields default to matching everything.
Blocklist patterns are pre-compiled at config load time using the safe compileRegex helper (500 character limit, fallback to /.^/ on failure). Malformed patterns are logged and skipped rather than crashing the router.
npm run dev # Build and run locally
npm run build # Build onlyThe router subscribes to and publishes on the following NATS subjects:
| Subject | Purpose |
|---|---|
chat.message.incoming.> |
Incoming chat messages from connectors |
command.register |
Command registration requests from modules |
broadcast.register |
Broadcast registration requests from modules |
command.unregister |
Command unregistration requests from modules |
broadcast.unregister |
Broadcast unregistration requests from modules |
admin.request.router |
Admin queries (rate-limit stats, command registry) |
stats.emit.request |
Stats collection requests |
stats.uptime |
Uptime queries |
| Subject | Purpose |
|---|---|
command.execute.<uuid> |
Dispatch a matched command to its handler module |
broadcast.message.<uuid> |
Forward a message to a broadcast subscriber |
control.registerCommands |
Prompt all modules to re-register commands |
control.registerBroadcasts |
Prompt all modules to re-register broadcasts |
control.registerCommands.<name> |
Prompt a specific module to re-register |
chat.notice.outgoing.irc.<instance> |
Send an IRC NOTICE (rate-limit notifications, 15s cooldown per user) |
help.remove |
Remove help entries for a module |
admin.response.router.ratelimit-stats |
Admin response with rate-limit data |
admin.response.router.command-registry |
Admin response with command registry data |
Modules register commands by publishing to command.register:
{
"type": "command.register",
"commandUUID": "weather-irc-libera",
"commandDisplayName": "weather",
"platform": "irc",
"network": "libera",
"channel": ".*",
"regex": "^!weather\\s+(.*)",
"platformPrefixAllowed": true,
"nickPrefixAllowed": true,
"ratelimit": {
"mode": "drop",
"level": "user",
"limit": 5,
"interval": "30s"
}
}| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "command.register" |
commandUUID |
Yes | Unique identifier for this command registration |
commandDisplayName |
No | Human-readable name for logs and admin UI |
platform |
No | Regex for platform matching (default ".*") |
network |
No | Regex for network matching (default ".*") |
instance |
No | Regex for instance matching (default ".*") |
channel |
No | Regex for channel matching (default ".*") |
user |
No | Regex for user matching (default ".*") |
nick |
No | Regex for nick matching (default ".*") |
regex |
Yes | Regex pattern to match against the command text |
platformPrefixAllowed |
Yes | Whether the platform's common prefix (e.g. !) is accepted |
nickPrefixAllowed |
No | Whether the bot's nick can prefix the command |
ratelimit |
Yes | Rate limiting configuration (see below) |
| Field | Required | Description |
|---|---|---|
mode |
Yes | "drop" (discard excess) or "enqueue" (queue for later execution) |
level |
Yes | Granularity: "global", "platform", "instance", "channel", or "user" |
notificationCooldown |
No | Seconds between rate-limit notices to the same user (default: 15) |
limit |
Yes | Max allowed executions per interval (0 disables rate limiting) |
interval |
Yes | Time window, e.g. "30s", "5m", "1h" |
Modules register broadcasts by publishing to broadcast.register:
{
"type": "broadcast.register",
"broadcastUUID": "logger-all",
"broadcastDisplayName": "logger",
"platform": ".*",
"channel": ".*",
"messageFilterRegex": ".*"
}| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "broadcast.register" |
broadcastUUID |
Yes | Unique identifier for this broadcast registration |
broadcastDisplayName |
No | Human-readable name for logs |
platform |
No | Regex for platform matching (default ".*") |
network |
No | Regex for network matching (default ".*") |
instance |
No | Regex for instance matching (default ".*") |
channel |
No | Regex for channel matching (default ".*") |
user |
No | Regex for user matching (default ".*") |
nick |
No | Regex for nick matching (default ".*") |
messageFilterRegex |
No | Additional regex filter on message text |
┌─────────────┐
Connectors ──────────►│ │
(IRC, Discord, ...) │ Router │────► command.execute.<uuid>
│ │────► broadcast.message.<uuid>
│ │
Modules ──────────────►│ │◄──── admin.request.router
(register commands) │ │
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
CommandRegistry Broadcast RateLimiter
Registry (queue + cleanup)
- A connector receives a chat message and publishes it to
chat.message.incoming.<platform>.<network>.<instance>.<channel> - The router parses the message and checks the blocklist — blocked messages are dropped
- The router checks the command registry for matching commands, handling prefix stripping (platform prefix or nick addressing)
- The router checks the broadcast registry for matching broadcasts
- If no commands or broadcasts match, the message is dropped
- For each matching command, the rate limiter is consulted:
- If allowed: publish to
command.execute.<uuid> - If rate-limited in
dropmode: silently discard - If rate-limited in
enqueuemode: queue for later processing
- If allowed: publish to
- For each matching broadcast: publish to
broadcast.message.<uuid>
| Component | File | Purpose |
|---|---|---|
main.mts |
Entry point | NATS setup, subscription wiring, startup |
CommandRegistry |
lib/command-registry.mts |
Stores registered commands, matches incoming text against command regexes |
BroadcastRegistry |
lib/broadcast-registry.mts |
Stores registered broadcasts, matches incoming messages against scope + filter regexes |
RateLimiter |
lib/rate-limiter.mts |
Tracks execution counts per key, enforces limits, processes queued commands |
PlatformNotifier |
lib/notifier.mts |
Sends rate-limit notices to users (IRC NOTICE support) |
message-handler |
lib/message-handler.mts |
Core routing logic — blocklist check, command/broadcast matching, dispatch |
registration-handler |
lib/registration-handler.mts |
Processes command.register and broadcast.register messages |
admin-handler |
lib/admin-handler.mts |
Handles admin queries for rate-limit stats and command registry inspection |
stats-handler |
lib/stats-handler.mts |
Responds to stats/uptime requests |
router-config |
lib/router-config.mts |
Loads and validates the YAML config file |
compile-regex |
lib/compile-regex.mts |
Safe regex compilation with ReDoS protection (500 char limit, fallback to /.^/) |
nats-setup |
lib/nats-setup.mts |
Establishes NATS connection from environment variables |
The router exposes both shared libeevee metrics and router-specific ones at http://localhost:<HTTP_API_PORT>/metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
broadcasts_total |
Counter | module, broadcast_uuid, platform, network, channel | Broadcasts processed |
registrations_total |
Counter | module, type, result | Registration events |
rate_limits_total |
Counter | module, command_uuid, action, mode | Rate limit events |
messages_total |
Counter | module, direction, result | Messages received (from libeevee) |
commands_total |
Counter | module, result | Commands dispatched (from libeevee) |
# Install dependencies
npm install
# Lint
npm test
# Build
npm run build
# Run locally (build + execute)
npm run dev
# Update libeevee
npm run update-libraries- Node.js ≥ 24.0.0
- A running NATS server with token auth
Contributions are welcome! Open an issue, fork, branch, PR. Run npm run build before submitting — it lints and compiles.
CC BY-NC-SA 4.0 — see LICENSE for the full text.