Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00
..

@elizaos/plugin-discord

A Discord connector plugin for elizaOS, enabling rich integration with Discord servers for managing interactions, voice, and message handling.

Features

  • Handle server join events and manage initial configurations
  • Voice event management via the voice manager
  • Manage and process new messages with the message manager
  • Slash command registration and interaction handling
  • Support for Discord attachments and media files
  • Voice channel join/leave functionality
  • Media transcription for voice and attachments
  • Channel restriction support (limit bot to specific channels)
  • Robust permissions management and audit event tracking
  • Event-driven architecture with comprehensive event handling
  • History backfill with efficient batch processing

Installation

As this is a workspace package, it is installed as part of the elizaOS monorepo:

bun install

Configuration

The plugin requires the following environment variables:

# Discord API Credentials (Required)
DISCORD_APPLICATION_ID=your_application_id
DISCORD_API_TOKEN=your_api_token

# Channel Restrictions (Optional)
# Comma-separated list of Discord channel IDs to restrict the bot to.
# If not set, the bot operates in all channels.
CHANNEL_IDS=123456789012345678,987654321098765432

# Listen-only channels (Optional)
# Comma-separated list of channel IDs where the bot will only listen (not respond).
DISCORD_LISTEN_CHANNEL_IDS=123456789012345678

# Voice Channel (Optional)
# ID of the voice channel the bot should auto-join when scanning a guild.
# If not set, the bot selects based on member activity.
DISCORD_VOICE_CHANNEL_ID=123456789012345678

# Behavior Settings (Optional)
# If true, ignore messages from other bots (default: true)
DISCORD_SHOULD_IGNORE_BOT_MESSAGES=true

# If true, ignore direct messages by default (default: true).
# DMs can still be allowed explicitly via DISCORD_ALLOW_FROM / pairing allowlist.
DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES=true

# If true, only respond when explicitly @mentioned (default: true)
DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS=true

# Generation Timeout (Optional)
# Wall-clock budget for generating a single reply before the bot gives up and
# posts "I timed out while generating that reply." On timeout the underlying
# model call is ABORTED (the abort signal threads through to runtime.useModel /
# the provider fetch), so an orphaned run cannot keep burning tokens or race a
# late response into the same channel.
#   Default: 120000 (2 min). Minimum: 30000 (values below the floor are raised).
#   Set to 0 to DISABLE the timeout entirely (await generation to completion —
#   useful for long media jobs like video). Falls back to MESSAGE_TIMEOUT_MS,
#   then to the media-generation timeout, when unset.
# For heavy-context / multi-step agentic turns on cloud models, raise this
# (e.g. 240000) rather than leaving replies to hit the 2-min cap.
DISCORD_GENERATION_TIMEOUT_MS=120000

# Testing (Optional)
DISCORD_TEST_CHANNEL_ID=123456789012345678

# Owner Alias Override (Optional)
# JSON array of Discord user IDs that intentionally alias to the canonical
# Eliza owner entity. Do not put ordinary Discord application team members here;
# they stay auditable as separate connector-admin identities.
ELIZA_DISCORD_OWNER_USER_IDS_JSON='["123456789012345678"]'

Settings can also be configured in your character file under settings.discord:

{
  "settings": {
    "discord": {
      "shouldIgnoreBotMessages": true,
      "shouldIgnoreDirectMessages": true,
      "shouldRespondOnlyToMentions": true,
      "allowedChannelIds": ["123456789012345678"]
    }
  }
}

Usage

{
  "plugins": ["@elizaos/plugin-discord"]
}

Slash Command Permissions

The plugin uses a hybrid permission system that combines Discord's native features with elizaOS-specific controls.

Permission Layers

Commands go through multiple permission checks in this order:

  1. Discord Native Checks (before interaction fires):

    • User must have required Discord permissions
    • Command must be available in the current context (guild vs DM)
  2. elizaOS Channel Whitelist (if CHANNEL_IDS is set):

    • Commands only work in whitelisted channels
    • Unless command has bypassChannelWhitelist: true
  3. Custom Validator (if provided):

    • Runs custom validation logic
    • Full programmatic control

Registering Commands

import { PermissionFlagsBits } from "discord.js";

// Simple command (works everywhere)
const helpCommand = {
  name: "help",
  description: "Show help information",
};

// Guild-only command
const serverInfoCommand = {
  name: "serverinfo",
  description: "Show server information",
  guildOnly: true,
};

// Requires Discord permission
const configCommand = {
  name: "config",
  description: "Configure bot settings",
  requiredPermissions: PermissionFlagsBits.ManageGuild,
};

// Bypasses channel whitelist
const utilityCommand = {
  name: "export",
  description: "Export data",
  bypassChannelWhitelist: true,
};

// Advanced: custom validation
const adminCommand = {
  name: "admin",
  description: "Admin-only command",
  validator: async (interaction, runtime) => {
    const adminIds = runtime.getSetting("ADMIN_USER_IDS")?.split(",") ?? [];
    return adminIds.includes(interaction.user.id);
  },
};

// Register commands
await runtime.emitEvent(["DISCORD_REGISTER_COMMANDS"], {
  commands: [
    helpCommand,
    serverInfoCommand,
    configCommand,
    utilityCommand,
    adminCommand,
  ],
});

Permission Options

Option Type Description
guildOnly boolean If true, command only works in guilds (not DMs)
bypassChannelWhitelist boolean If true, bypasses CHANNEL_IDS restrictions
requiredPermissions bigint | string Discord permission bitfield (e.g., PermissionFlagsBits.ManageGuild)
contexts number[] Raw Discord contexts (0=Guild, 1=BotDM, 2=PrivateChannel)
guildIds string[] Register only in specific guilds (instant updates)
validator function Custom validation function for advanced logic

Common Permission Values

From Discord.js PermissionFlagsBits:

  • ManageGuild - Server settings
  • ManageChannels - Channel management
  • ManageMessages - Delete messages
  • BanMembers - Ban users
  • KickMembers - Kick users
  • ModerateMembers - Timeout users
  • ManageRoles - Role management
  • Administrator - Full access

Actions / Providers

No actions or providers are registered. The plugin operates entirely through services and events. Credential pairing is handled by connector account providers and owner-only slash commands.

Event Types

The plugin emits the following Discord-specific events:

Event Description
DISCORD_MESSAGE_RECEIVED When a message is received
DISCORD_MESSAGE_SENT When a message is sent
DISCORD_SLASH_COMMAND When a slash command is invoked
DISCORD_MODAL_SUBMIT When a modal form is submitted
DISCORD_REACTION_RECEIVED When a reaction is added to a message
DISCORD_REACTION_REMOVED When a reaction is removed from a message
DISCORD_WORLD_JOINED When the bot joins a guild
DISCORD_SERVER_CONNECTED When connected to a server
DISCORD_USER_JOINED When a user joins a guild
DISCORD_USER_LEFT When a user leaves a guild
DISCORD_VOICE_STATE_CHANGED When voice state changes
DISCORD_CHANNEL_PERMISSIONS_CHANGED When channel permissions change
DISCORD_ROLE_PERMISSIONS_CHANGED When role permissions change
DISCORD_MEMBER_ROLES_CHANGED When a member's roles change
DISCORD_ROLE_CREATED When a role is created
DISCORD_ROLE_DELETED When a role is deleted

Key Components

DiscordService

Main service class that extends elizaOS Service:

  • Handles authentication and session management
  • Manages Discord client connection
  • Processes events and interactions
  • Supports channel history backfill with efficient batch processing

MessageManager

  • Processes incoming messages and responses
  • Handles attachments and media files
  • Supports message formatting and templating
  • Manages conversation context

VoiceManager

  • Manages voice channel interactions
  • Handles joining and leaving voice channels
  • Processes voice events and audio streams
  • Integrates with transcription services

AttachmentManager

  • Downloads and processes Discord attachments
  • Supports various media types
  • Integrates with media transcription

Developer Guide

Custom Slash Commands

Register slash commands via the DISCORD_REGISTER_COMMANDS event, then listen for interactions:

// Register custom slash commands
await runtime.emitEvent(["DISCORD_REGISTER_COMMANDS"], {
  commands: [
    {
      name: "mycommand",
      description: "My custom command",
      options: [
        {
          name: "input",
          description: "User input",
          type: 3, // STRING type
          required: true,
        },
      ],
    },
    {
      name: "serverinfo",
      description: "Get server information",
      guildOnly: true, // Only works in guilds, not DMs
    },
  ],
});

// Listen for slash command events to handle the interaction
runtime.registerEvent({
  name: "DISCORD_SLASH_COMMAND",
  handler: async (payload) => {
    const { interaction, client, commands } = payload;

    if (interaction.commandName === "mycommand") {
      const input = interaction.options.getString("input");
      await interaction.reply(`You said: ${input}`);
    }
  },
});

Building on the Listen System

The DISCORD_LISTEN_CHANNEL_IDS setting creates "listen-only" channels where the bot receives messages but doesn't respond. This is useful for:

  • Monitoring channels - Track activity without interrupting conversations
  • Data collection - Gather messages for analysis or training
  • Conditional responses - Build custom logic that decides when to respond
// Check if a channel is listen-only
const listenChannels = runtime.getSetting("DISCORD_LISTEN_CHANNEL_IDS");
const listenChannelIds = listenChannels?.split(",").map((s) => s.trim()) || [];

runtime.registerEvent({
  name: "DISCORD_MESSAGE_RECEIVED",
  handler: async (payload) => {
    const { message } = payload;
    const channelId = message.content.channelId;

    if (listenChannelIds.includes(channelId)) {
      // This is a listen-only channel - process without responding
      await processMessageSilently(message);
    }
  },
});

Handling Modal and Component Interactions

Modal submits and button components bypass channel whitelists to support multi-step UI flows:

// Listen for modal submissions
runtime.registerEvent({
  name: "DISCORD_MODAL_SUBMIT",
  handler: async (payload) => {
    const { interaction } = payload;
    const fieldValue = interaction.fields.getTextInputValue("myField");
    await interaction.reply(`Received: ${fieldValue}`);
  },
});

Permission Audit System

The plugin includes a comprehensive permission audit system that tracks all permission changes with full audit log integration. This is useful for:

  • Security monitoring - Detect unauthorized permission escalations
  • Compliance logging - Maintain records of who changed what and when
  • Bot self-protection - Detect when the bot's permissions are modified

Event Payloads

DISCORD_CHANNEL_PERMISSIONS_CHANGED - When channel overwrites change:

interface ChannelPermissionsChangedPayload {
  runtime: IAgentRuntime;
  guild: { id: string; name: string };
  channel: { id: string; name: string };
  target: { type: "role" | "user"; id: string; name: string };
  action: "CREATE" | "UPDATE" | "DELETE";
  changes: Array<{
    permission: string; // e.g., 'ManageMessages', 'Administrator'
    oldState: "ALLOW" | "DENY" | "NEUTRAL";
    newState: "ALLOW" | "DENY" | "NEUTRAL";
  }>;
  audit: {
    executorId: string;
    executorTag: string;
    reason: string | null;
  } | null;
}

DISCORD_ROLE_PERMISSIONS_CHANGED - When role permissions change:

interface RolePermissionsChangedPayload {
  runtime: IAgentRuntime;
  guild: { id: string; name: string };
  role: { id: string; name: string };
  changes: PermissionDiff[];
  audit: AuditInfo | null;
}

DISCORD_MEMBER_ROLES_CHANGED - When a member's roles change:

interface MemberRolesChangedPayload {
  runtime: IAgentRuntime;
  guild: { id: string; name: string };
  member: { id: string; tag: string };
  added: Array<{ id: string; name: string; permissions: string[] }>;
  removed: Array<{ id: string; name: string; permissions: string[] }>;
  audit: AuditInfo | null;
}

DISCORD_ROLE_CREATED / DISCORD_ROLE_DELETED - Role lifecycle:

interface RoleLifecyclePayload {
  runtime: IAgentRuntime;
  guild: { id: string; name: string };
  role: { id: string; name: string; permissions: string[] };
  audit: AuditInfo | null;
}

Example: Security Monitoring

import { DiscordEventTypes } from "@elizaos/plugin-discord";
import { logger } from "@elizaos/core";

// Alert on dangerous permission grants
runtime.registerEvent({
  name: DiscordEventTypes.CHANNEL_PERMISSIONS_CHANGED,
  handler: async (payload) => {
    const dangerousPerms = ["Administrator", "ManageGuild", "ManageRoles"];

    for (const change of payload.changes) {
      if (
        dangerousPerms.includes(change.permission) &&
        change.newState === "ALLOW"
      ) {
        logger.warn(
          { channel: payload.channel.name, target: payload.target.name, permission: change.permission, grantedBy: payload.audit?.executorTag || "Unknown" },
          "[SecurityMonitor] Dangerous permission granted",
        );
      }
    }
  },
});

// Track role escalations
runtime.registerEvent({
  name: DiscordEventTypes.MEMBER_ROLES_CHANGED,
  handler: async (payload) => {
    const adminRoles = payload.added.filter((r) =>
      r.permissions.includes("Administrator"),
    );

    if (adminRoles.length > 0) {
      logger.warn(
        { member: payload.member.tag, roles: adminRoles.map((r) => r.name), grantedBy: payload.audit?.executorTag || "Unknown" },
        "[SecurityMonitor] Admin role granted",
      );
    }
  },
});

// Log all role creations
runtime.registerEvent({
  name: DiscordEventTypes.ROLE_CREATED,
  handler: async (payload) => {
    logger.info(
      { role: payload.role.name, permissions: payload.role.permissions, createdBy: payload.audit?.executorTag || "Unknown" },
      "[SecurityMonitor] New role created",
    );
  },
});

Bot Self-Protection

Monitor when the bot's own permissions change:

runtime.registerEvent({
  name: DiscordEventTypes.MEMBER_ROLES_CHANGED,
  handler: async (payload) => {
    const botId = runtime.getSetting("DISCORD_APPLICATION_ID");

    if (payload.member.id === botId && payload.removed.length > 0) {
      logger.error(
        { removed: payload.removed.map((r) => r.name), by: payload.audit?.executorTag || "Unknown" },
        "[SecurityMonitor] Bot roles removed",
      );
    }
  },
});

Cross-Core Compatibility

This plugin includes a type-level compatibility layer (compat.ts) so it typechecks against both old and new versions of @elizaos/core. It exports widened types (ICompatRuntime, WorldCompat, RoomCompat) that allow either serverId or messageServerId on World/Room objects and on the ensureConnection/ensureWorldExists/ensureRoomExists runtime methods. There is no runtime proxy — it is purely TypeScript declarations consumed where the runtime is referenced (service.ts, messages.ts, voice.ts, discord-interactions.ts, discord-history.ts).

Testing

The plugin includes a test suite for validating functionality:

bun run test

Notes

  • Ensure that your .env file includes the required DISCORD_API_TOKEN
  • The bot requires appropriate Discord permissions (send messages, connect to voice, etc.)
  • If no token is provided, the plugin will load but remain non-functional with appropriate warnings
  • The plugin uses discord.js v14 (^14.26.4) with comprehensive intent support
  • Slash commands and modal/component interactions bypass channel whitelists