18 KiB
@elizaos/plugin-discord
Discord connector plugin for elizaOS — connects an Eliza agent to Discord servers via the Discord.js gateway.
Purpose / role
This plugin registers the DiscordService (and companion services) with the elizaOS runtime, giving an Eliza agent the ability to send and receive messages, handle voice, manage slash commands, track permission changes, and bridge the Discord desktop app via a local IPC connector. It is auto-enabled when discord appears in the character's connector keys (autoEnable.connectorKeys: ["discord"]). No actions or providers are registered; all behavior flows through services and events.
Plugin surface
Services
| Name | Type key | Purpose |
|---|---|---|
DiscordService |
"discord" |
Main gateway service — connects to Discord API, handles messages, voice, slash commands, reactions, history backfill, and emits DiscordEventTypes.* events |
DiscordOwnerPairingServiceImpl |
"OWNER_PAIRING_DISCORD" |
Registers the /eliza-pair slash command; relays pairing codes to the backend owner-bind service and DMs login links |
DiscordUserAccountScraperImpl |
"discord_user_account_scraper" |
Scrapes message history and DM inboxes from the Discord desktop app via CDP/browser automation |
Routes (registered with rawPath: true)
| Method | Path | File | Purpose |
|---|---|---|---|
| GET | /api/setup/discord/status |
setup-routes.ts |
OAuth/IPC connection state |
| POST | /api/setup/discord/start |
setup-routes.ts |
Initiate authorization |
| POST | /api/setup/discord/cancel |
setup-routes.ts |
Tear down session |
| GET | /api/discord/guilds |
data-routes.ts |
List guilds after auth |
| GET | /api/discord/channels |
data-routes.ts |
List channels for a guild |
| POST | /api/discord/subscriptions |
data-routes.ts |
Subscribe to channel IDs |
Events emitted (from DiscordEventTypes in types.ts)
DISCORD_MESSAGE_RECEIVED, DISCORD_MESSAGE_SENT, DISCORD_SLASH_COMMAND, DISCORD_MODAL_SUBMIT, DISCORD_REACTION_RECEIVED, DISCORD_REACTION_REMOVED, DISCORD_WORLD_JOINED, DISCORD_SERVER_CONNECTED, DISCORD_USER_JOINED, DISCORD_USER_LEFT, DISCORD_VOICE_STATE_CHANGED, DISCORD_CHANNEL_PERMISSIONS_CHANGED, DISCORD_ROLE_PERMISSIONS_CHANGED, DISCORD_MEMBER_ROLES_CHANGED, DISCORD_ROLE_CREATED, DISCORD_ROLE_DELETED, DISCORD_LISTEN_CHANNEL_MESSAGE, DISCORD_NOT_IN_CHANNELS_MESSAGE
Actions / providers / evaluators
None registered. The plugin operates entirely through services and events.
Layout
plugins/plugin-discord/
index.ts Plugin definition + public barrel exports; init registers connector provider, reads env vars, logs banner
index.browser.ts Browser-unavailable entry (Discord.js needs Node APIs; logs and exports plugin shape)
banner.ts printBanner — ANSI startup banner with tiered invite-URL permissions
service.ts DiscordService — main gateway service (discord.js Client, message/voice/interaction handling)
voice.ts VoiceManager helper called by DiscordService for audio RX/TX, STT integration
config.ts Config types: DiscordConfig, DiscordAccountConfig, DiscordGuildEntry, DiscordChannelConfig, etc.
types.ts DiscordEventTypes enum, all event payload interfaces, DiscordSettings, error classes, snowflake helpers
accounts.ts Multi-account helpers: resolveDiscordAccount, normalizeDiscordToken, listDiscordAccountIds
account-client-pool.ts Per-account client/manager state pool (DiscordAccountClientState)
allowlist.ts Message gating: validateMessageAllowed, resolveDiscordAllowListMatch, resolveDiscordChannelConfig
permissions.ts Permission tiers, generateInviteUrl, getPermissionValues
permissionEvents.ts Elevated permissions helpers (hasElevatedPermissions, isElevatedRole)
owner-pairing-service.ts DiscordOwnerPairingServiceImpl — /eliza-pair slash command, DM login links
discord-local-service.ts DiscordLocalService — IPC connector to Discord desktop app (OAuth + local RPC socket)
setup-routes.ts HTTP routes for connector setup state machine (/api/setup/discord/*)
data-routes.ts HTTP routes for post-auth data (/api/discord/guilds, channels, subscriptions)
slash-commands.ts Slash command registry and dispatcher
native-commands.ts Utilities for building Discord slash commands with button-based argument menus
catalog-commands.ts Registers connector-neutral catalog commands (think, reasoning, views, knowledge, plugins, ...) into the slash-command registry, deduplicating against hand-written built-ins
interactions.ts Maps neutral agent InteractionBlock output to Discord ActionRow/button components; decodes callback custom_id on button click
messaging.ts Text helpers: chunkDiscordText, escapeDiscordMarkdown, extractAllUserMentions, etc.
messages.ts Message creation/sending logic used by DiscordService
message-coalesce.ts Debouncing/coalescing of rapid inbound messages before processing
discord-history.ts Channel history backfill (paginated fetch, batch storage)
discord-events.ts Discord.js event binding in DiscordService (GuildCreate, MessageCreate, etc.)
discord-interactions.ts Interaction handler (slash commands, buttons, modals)
discord-reactions.ts Reaction add/remove handlers
discord-profiles.ts Profile resolution helpers (avatar, entity display name)
discord-avatar-cache.ts Avatar URL caching utilities
profileSync.ts Bot username/avatar sync on startup (DISCORD_SYNC_PROFILE)
discord-commands.ts Command sync utilities (REST-based command registration)
connector-account-provider.ts Registers this plugin as a connector account provider with ConnectorAccountManager
sensitive-request-adapter.ts Wraps DM-sent sensitive requests for approval flows
inbound-envelope.ts Normalises raw Discord messages into elizaOS Memory objects
attachments.ts Attachment download and media type detection
addressing.ts isDiscordUserAddressed — detects whether the bot is being addressed in a message
identity.ts Owner/entity ID resolution + world/entity metadata (resolveElizaOwnerEntityId, resolveDiscordRuntimeEntityId, buildDiscordWorldMetadata)
debouncer.ts Generic debounce utility used by message-coalesce
typing.ts Typing-indicator controller (start/stop) for a channel
status-reactions.ts Status reaction scope helper (acknowledge inbound messages with an emoji)
draft-chunking.ts Streaming draft chunking logic
draft-stream.ts Draft streaming over Discord message edits
staleness.ts Stale-message guard (tag/skip/ignore behavior for out-of-sequence messages)
auto-enable.ts Character-level auto-enable resolution
environment.ts Env var validation (DISCORD_API_TOKEN required check)
compat.ts Type-only cross-core shim (ICompatRuntime/WorldCompat/RoomCompat widen serverId/messageServerId)
constants.ts DISCORD_SERVICE_NAME = "discord"
utils.ts Misc runtime helpers (getMessageService, normalizeDiscordMessageText, etc.)
user-account-scraper/
service.ts DiscordUserAccountScraperImpl — CDP-based message and DM inbox scraper
discord-browser-scraper.ts Browser-based scraper logic
discord-desktop-cdp.ts Desktop CDP probe and tab management utilities
index.ts Barrel re-export
actions/
actionResultSemantics.ts Action result helpers
setup-credentials.ts Credential preset definitions used by /setup slash command
prompts/ Source JSON for generated action/provider/evaluator docs
generated/specs/ Auto-generated canonical action/provider doc specs (do not edit)
__tests__/ Vitest unit tests (co-located fast tests)
test/ Vitest unit + live integration tests
helpers/ Shared test helpers (http.ts, pglite-runtime.ts)
live/ Live integration specs (require real Discord credentials)
tests.ts DiscordTestSuite for elizaOS plugin test runner
Commands
Only scripts present in this package's package.json:
bun run --cwd plugins/plugin-discord build # compile with build.ts -> dist/
bun run --cwd plugins/plugin-discord dev # hot-rebuild with bun --hot build.ts
bun run --cwd plugins/plugin-discord test # vitest run (unit tests)
bun run --cwd plugins/plugin-discord typecheck # tsc --noEmit
bun run --cwd plugins/plugin-discord lint # biome check --write --unsafe
bun run --cwd plugins/plugin-discord format # biome format --write
bun run --cwd plugins/plugin-discord test:e2e # live smoke test via app-core runner
bun run --cwd plugins/plugin-discord clean # rm dist + .turbo + generated .d.ts files
Config / env vars
| Variable | Required | Description |
|---|---|---|
DISCORD_API_TOKEN |
Yes | Bot token used to log in the Discord gateway client |
DISCORD_APPLICATION_ID |
Yes | Discord application (client) ID — used for slash command registration and invite URL generation |
DISCORD_BOT_TOKENS |
No | Comma-separated multi-account tokens (multi-account mode alternative to DISCORD_API_TOKEN) |
CHANNEL_IDS |
No | Comma-separated channel IDs the bot is restricted to (whitelist) |
DISCORD_LISTEN_CHANNEL_IDS |
No | Comma-separated channel IDs where the bot ingests messages but does NOT reply |
DISCORD_VOICE_CHANNEL_ID |
No | Voice channel ID to auto-join on guild scan; defaults to most-populated channel |
DISCORD_SHOULD_IGNORE_BOT_MESSAGES |
No | "false" to process messages from other bots (default: true — see DISCORD_DEFAULTS in environment.ts) |
DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES |
No | "false" to process DMs (default: true) |
DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS |
No | "false" to reply without an @-mention (default: true) |
DISCORD_DM_POLICY |
No | DM access policy: open / allowlist / pairing / disabled (default: pairing) |
DISCORD_ALLOW_FROM |
No | Comma-separated Discord user IDs allowed under the allowlist DM policy |
ELIZA_DISCORD_OWNER_USER_IDS_JSON |
No | JSON array of Discord user IDs that should alias to the canonical Eliza owner entity. Use only for the actual owner or deliberate owner aliases; Discord application team members are kept as separate entities; accepted, non-read-only members are seeded as connector admins (pending invitees and read_only members get no standing), and more can be added via ELIZA_ROLES_CONNECTOR_ADMINS_JSON. |
DISCORD_AUTO_REPLY |
No | "true" to auto-generate replies; default false (messages are ingested but not answered) |
DISCORD_USER_INSTALL |
No | "true" to register commands as user-installable and available in group DMs / DMs-with-others (contexts guild+bot-DM+private-channel, integration types guild+user install). Requires the Discord app to be configured as User Install in the developer portal, or Discord rejects registration. Default false (guild + bot-DM only). |
DISCORD_SYNC_PROFILE |
No | "false" to skip bot profile sync on startup (default: true) |
DISCORD_IPC_DIR |
No | Override the IPC socket directory searched by DiscordLocalService when connecting to the Discord desktop app |
DISCORD_GENERATION_TIMEOUT_MS |
No | Milliseconds cap for generation before the pending Discord message is discarded |
MESSAGE_TIMEOUT_MS |
No | Fallback generation timeout in ms (used when DISCORD_GENERATION_TIMEOUT_MS is not set) |
DISCORD_TEST_CHANNEL_ID |
No | Channel used by DiscordTestSuite |
All settings can also be provided in the character file under settings.discord (as DiscordConfig / DiscordAccountConfig) — character settings override env vars.
How to extend
Add a new slash command — either call addCommand(commandSpec) (from slash-commands.ts) with a SlashCommand-shaped object that includes an execute(interaction, runtime) function, or emit the DISCORD_REGISTER_COMMANDS event with an array of DiscordSlashCommand objects (from types.ts) for external registration. Handle responses by listening for DISCORD_SLASH_COMMAND or DISCORD_MODAL_SUBMIT events.
Add a new event handler — subscribe to a DiscordEventTypes.* string event on the runtime from any plugin or service. All event payload types are in types.ts (DiscordEventPayloadMap).
Add a new HTTP route — append a Route object to discordSetupRoutes (in setup-routes.ts) or discordDataRoutes (in data-routes.ts), or export a new route array from a new file and add it to the routes array in index.ts.
Add a new service — create a class extending Service from @elizaos/core, export it, and add it to the services array in index.ts.
Conventions / gotchas
- Discord snowflake IDs are not UUIDs. Use
stringToUuid(discordId)to store them in UUID fields. UsecreateUniqueUuid(runtime, discordId)forworldId/roomId(agent-namespaced). Never useasUUID()on raw Discord IDs — it throws. messageServerIdis the correct field for server IDs onRoomandWorldobjects (notserverId).compat.tsis a type-only shim (ICompatRuntime/WorldCompat/RoomCompat) that widens these fields so the code typechecks across core versions — there is no runtime proxy.- Native dependencies.
@discordjs/opusandlibsodium-wrappershave native binaries. They must build successfully in the Node.js environment; the plugin does not run in browsers (seeeliza.platforms: ["node"]). - Voice requires ffmpeg. The
fluent-ffmpegdep expects a systemffmpegbinary onPATHfor audio processing. - Multi-account mode is activated by setting
DISCORD_BOT_TOKENS(comma-separated) instead ofDISCORD_API_TOKEN. Seeaccounts.tsfor resolution order. autoReplyis false by default inDiscordSettings— messages are ingested but the agent does not auto-reply unless explicitly enabled.- See root
AGENTS.mdfor repo-wide architecture rules, logger-only logging, ESM conventions, and naming standards.
⛔ NON-NEGOTIABLE — evidence, trajectories & real end-to-end tests
The binding, repo-wide standard is AGENTS.md. Read it. Nothing in this package is done until it is proven done — a reviewer must confirm it works without reading the code, from the artifacts you attach. This applies to every feature, fix, refactor, and chore here. "Tests pass" is not proof; "CI is green" is not proof.
- Record AND read model trajectories. Capture the actual inputs and outputs of the model
from a live LLM — not the deterministic proxy, not a mock: the prompt, the
providers/context, the raw model output, every tool/action call, and the result. Then open
the trajectory and review it by hand. A captured-but-unread trajectory is not evidence
(
packages/scenario-runner/bin/eliza-scenarios run <scenario> --report <out>). - Real, full-featured E2E — no larp. Every feature ships detailed end-to-end tests that drive the real path end to end. Not the happy "front door" only: cover error paths, edge/empty/invalid input, concurrency, roles/permissions, and adversarial input. A test that asserts against a mock/stub/fixture standing in for the thing under test does not count. If the real model/device/chain/connector/account is hard to reach, make it reachable — that is the work, not an excuse to mock. If the existing tests here are shallow or mocked, fixing them is part of your change.
- Screenshots + logs at every phase, plus a complete walkthrough video/run-through of
the entire feature or view, start to finish (
bun run test:e2e:record). - Manually review every artifact the change touches — never just the green check: client
logs (console + network), server logs (
[ClassName] …), the model trajectories in and out, before/after full-page screenshots, and the domain artifacts listed below for this package. - No residuals. No shortcuts. The goal is not "done" — it is everything done. Clear every blocker by the hard path: build the real architecture, stand up the real model/device/service, actually test it. Never leave a TODO, a stub, a stepping-stone, or a "follow-up." When unsure, research thoroughly, weigh the options, and ship the best, highest-effort, production-ready version. Keep going until every possibility is exhausted.
Artifacts → attached inline in the PR (MP4 video, JPG screenshots, logs in <details>); attach each evidence type or
explicitly mark it N/A with a reason — never leave it blank. If develop moved and changed
behavior, re-capture evidence; stale proof is worse than none.
Capture & manually review for this package — platform connector:
- A real (or sandbox-account) round-trip on the platform: inbound message → agent → outbound reply, captured as logs and a screenshot/recording of the actual conversation.
- The raw inbound event/webhook payload and the outbound API request/response, with IDs mapped correctly (
stringToUuid/createUniqueUuid). - Attachments, threads/replies, edits, multi-account, and rate-limit/error paths — not just a single text ping.
- The agent trajectory for the turn the connector drove.