Overview
The @elizaos/plugin-discord
package provides comprehensive Discord integration for ElizaOS agents. It enables agents to operate as fully-featured Discord bots with support for text channels, voice channels, direct messages, slash commands, and media processing.
This plugin handles all Discord-specific functionality including:
- Initializing and managing the Discord bot connection
- Processing messages and interactions across multiple servers
- Managing voice channel connections and audio processing
- Handling media attachments and transcription
- Implementing Discord-specific actions and state providers
- Supporting channel restrictions and permission management
Architecture Overview
Core Components
Discord Service
The DiscordService
class is the main entry point for Discord functionality:
export class DiscordService extends Service implements IDiscordService {
static serviceType: string = DISCORD_SERVICE_NAME;
client: DiscordJsClient | null;
character: Character;
messageManager?: MessageManager;
voiceManager?: VoiceManager;
private allowedChannelIds?: string[];
constructor(runtime: IAgentRuntime) {
super(runtime);
// Initialize Discord client with proper intents
// Set up event handlers
// Parse channel restrictions
}
}
Key Responsibilities:
-
Client Initialization
- Creates Discord.js client with required intents
- Handles authentication with bot token
- Manages connection lifecycle
-
Event Registration
- Listens for Discord events (messages, interactions, etc.)
- Routes events to appropriate handlers
- Manages event cleanup on disconnect
-
Channel Restrictions
- Parses
CHANNEL_IDS
environment variable
- Enforces channel-based access control
- Filters messages based on allowed channels
-
Component Coordination
- Initializes MessageManager and VoiceManager
- Coordinates between different components
- Manages shared state and resources
Message Manager
The MessageManager
class handles all message-related operations:
export class MessageManager {
private client: DiscordJsClient;
private runtime: IAgentRuntime;
private inlinePositionalCallbacks: Map<string, (message: DiscordMessage, args: string) => void>;
async handleMessage(message: DiscordMessage): Promise<void> {
// Convert Discord message to ElizaOS format
// Process attachments
// Send to bootstrap plugin
// Handle response
}
async processAttachments(message: DiscordMessage): Promise<Content[]> {
// Download and process media files
// Generate descriptions for images
// Transcribe audio/video
}
}
Message Processing Flow:
-
Message Reception
// Discord message received
if (message.author.bot) return; // Ignore bot messages
if (!this.shouldProcessMessage(message)) return;
-
Format Conversion
const elizaMessage = await this.convertMessage(message);
elizaMessage.channelId = message.channel.id;
elizaMessage.serverId = message.guild?.id;
-
Attachment Processing
if (message.attachments.size > 0) {
elizaMessage.attachments = await this.processAttachments(message);
}
-
Response Handling
const callback = async (response: Content) => {
await this.sendResponse(message.channel, response);
};
Voice Manager
The VoiceManager
class manages voice channel operations:
export class VoiceManager {
private client: DiscordJsClient;
private runtime: IAgentRuntime;
private connections: Map<string, VoiceConnection>;
async joinChannel(channel: VoiceChannel): Promise<void> {
// Create voice connection
// Set up audio processing
// Handle connection events
}
async processAudioStream(stream: AudioStream): Promise<void> {
// Process incoming audio
// Send to transcription service
// Handle transcribed text
}
}
Voice Features:
-
Connection Management
- Join/leave voice channels
- Handle connection state changes
- Manage multiple connections
-
Audio Processing
- Capture audio streams
- Process voice activity
- Handle speaker changes
-
Transcription Integration
- Send audio to transcription services
- Process transcribed text
- Generate responses
Attachment Handler
Processes various types of Discord attachments:
export async function processAttachments(
attachments: Attachment[],
runtime: IAgentRuntime
): Promise<Content[]> {
const contents: Content[] = [];
for (const attachment of attachments) {
if (isImage(attachment)) {
// Process image with vision model
const description = await describeImage(attachment.url, runtime);
contents.push({ type: 'image', description });
} else if (isAudio(attachment)) {
// Transcribe audio
const transcript = await transcribeAudio(attachment.url, runtime);
contents.push({ type: 'audio', transcript });
}
}
return contents;
}
Event Processing Flow
1. Guild Join Event
client.on(Events.GuildCreate, async (guild: Guild) => {
// Create server room
await createGuildRoom(guild);
// Emit WORLD_JOINED event
runtime.emitEvent([DiscordEventTypes.GUILD_CREATE, EventType.WORLD_JOINED], {
world: convertGuildToWorld(guild),
runtime
});
// Register slash commands
await registerCommands(guild);
});
2. Message Create Event
client.on(Events.MessageCreate, async (message: DiscordMessage) => {
// Check permissions and filters
if (!shouldProcessMessage(message)) return;
// Process through MessageManager
await messageManager.handleMessage(message);
// Track conversation context
updateConversationContext(message);
});
3. Interaction Create Event
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
if (!interaction.isChatInputCommand()) return;
// Route to appropriate handler
const handler = commandHandlers.get(interaction.commandName);
if (handler) {
await handler(interaction, runtime);
}
});
Actions
chatWithAttachments
Handles messages that include media attachments:
export const chatWithAttachments: Action = {
name: "CHAT_WITH_ATTACHMENTS",
description: "Process and respond to messages with attachments",
async handler(runtime, message, state, options, callback) {
// Process attachments
const processedContent = await processAttachments(
message.attachments,
runtime
);
// Generate response considering attachments
const response = await generateResponse(
message,
processedContent,
runtime
);
// Send response
await callback(response);
}
};
joinVoice
Connects the bot to a voice channel:
export const joinVoice: Action = {
name: "JOIN_VOICE",
description: "Join a voice channel",
async handler(runtime, message, state, options, callback) {
const channelId = options.channelId || message.channelId;
const channel = await client.channels.fetch(channelId);
if (channel?.type === ChannelType.GuildVoice) {
await voiceManager.joinChannel(channel);
await callback({
text: `Joined voice channel: ${channel.name}`
});
}
}
};
Transcribes audio or video files:
export const transcribeMedia: Action = {
name: "TRANSCRIBE_MEDIA",
description: "Convert audio/video to text",
async handler(runtime, message, state, options, callback) {
const mediaUrl = options.url || message.attachments?.[0]?.url;
if (mediaUrl) {
const transcript = await transcribeAudio(mediaUrl, runtime);
await callback({
text: `Transcript: ${transcript}`
});
}
}
};
Providers
channelStateProvider
Provides current Discord channel context:
export const channelStateProvider: Provider = {
name: "CHANNEL_STATE",
description: "Current Discord channel information",
async get(runtime, message, state) {
const channelId = message.channelId;
const channel = await client.channels.fetch(channelId);
return {
channelId,
channelName: channel?.name,
channelType: channel?.type,
guildId: channel?.guild?.id,
guildName: channel?.guild?.name,
memberCount: channel?.guild?.memberCount
};
}
};
voiceStateProvider
Provides voice channel state information:
export const voiceStateProvider: Provider = {
name: "VOICE_STATE",
description: "Voice channel state and members",
async get(runtime, message, state) {
const voiceChannel = getCurrentVoiceChannel(message.serverId);
if (!voiceChannel) return null;
return {
channelId: voiceChannel.id,
channelName: voiceChannel.name,
members: voiceChannel.members.map(m => ({
id: m.id,
name: m.displayName,
speaking: m.voice.speaking
})),
connection: {
state: voiceConnection?.state,
ping: voiceConnection?.ping
}
};
}
};
Configuration
Environment Variables
# Required
DISCORD_APPLICATION_ID=123456789012345678
DISCORD_API_TOKEN=your-bot-token-here
# Optional Channel Restrictions
CHANNEL_IDS=123456789012345678,987654321098765432
# Voice Configuration
DISCORD_VOICE_CHANNEL_ID=123456789012345678
VOICE_ACTIVITY_THRESHOLD=0.5
# Testing
DISCORD_TEST_CHANNEL_ID=123456789012345678
Bot Permissions
Required Discord permissions:
const requiredPermissions = new PermissionsBitField([
// Text Permissions
PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.SendMessagesInThreads,
PermissionsBitField.Flags.CreatePublicThreads,
PermissionsBitField.Flags.CreatePrivateThreads,
PermissionsBitField.Flags.EmbedLinks,
PermissionsBitField.Flags.AttachFiles,
PermissionsBitField.Flags.ReadMessageHistory,
PermissionsBitField.Flags.AddReactions,
PermissionsBitField.Flags.UseExternalEmojis,
// Voice Permissions
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.Speak,
PermissionsBitField.Flags.UseVAD,
// Application Commands
PermissionsBitField.Flags.UseApplicationCommands
]);
Bot Invitation
Generate an invitation URL:
const inviteUrl = `https://discord.com/api/oauth2/authorize?` +
`client_id=${DISCORD_APPLICATION_ID}` +
`&permissions=${requiredPermissions.bitfield}` +
`&scope=bot%20applications.commands`;
Multi-Server Architecture
The plugin supports operating across multiple Discord servers simultaneously:
Server Isolation
Each server maintains its own:
- Conversation context
- User relationships
- Channel states
- Voice connections
// Server-specific context
const serverContext = new Map<string, ServerContext>();
interface ServerContext {
guildId: string;
conversations: Map<string, Conversation>;
voiceConnection?: VoiceConnection;
settings: ServerSettings;
}
Command Registration
Slash commands are registered per-server:
async function registerServerCommands(guild: Guild) {
const commands = [
{
name: 'chat',
description: 'Chat with the bot',
options: [{
name: 'message',
type: ApplicationCommandOptionType.String,
description: 'Your message',
required: true
}]
}
];
await guild.commands.set(commands);
}
Permission Management
Permission Checking
Before performing actions:
function checkPermissions(
channel: GuildChannel,
permissions: PermissionsBitField
): boolean {
const botMember = channel.guild.members.me;
if (!botMember) return false;
const channelPerms = channel.permissionsFor(botMember);
return channelPerms?.has(permissions) ?? false;
}
Error Handling
Handle permission errors gracefully:
try {
await channel.send(response);
} catch (error) {
if (error.code === 50013) { // Missing Permissions
logger.warn(`Missing permissions in channel ${channel.id}`);
// Try to notify in a channel where we have permissions
await notifyPermissionError(channel.guild);
}
}
Message Caching
Cache frequently accessed data:
const messageCache = new LRUCache<string, ProcessedMessage>({
max: 1000,
ttl: 1000 * 60 * 60 // 1 hour
});
Rate Limiting
Implement rate limiting for API calls:
const rateLimiter = new RateLimiter({
windowMs: 60000, // 1 minute
max: 30 // 30 requests per minute
});
Voice Connection Pooling
Reuse voice connections:
const voiceConnectionPool = new Map<string, VoiceConnection>();
async function getOrCreateVoiceConnection(
channel: VoiceChannel
): Promise<VoiceConnection> {
const existing = voiceConnectionPool.get(channel.guild.id);
if (existing?.state.status === VoiceConnectionStatus.Ready) {
return existing;
}
const connection = await createNewConnection(channel);
voiceConnectionPool.set(channel.guild.id, connection);
return connection;
}
Error Handling
Connection Errors
Handle Discord connection issues:
client.on('error', (error) => {
logger.error('Discord client error:', error);
// Attempt reconnection
scheduleReconnection();
});
client.on('disconnect', () => {
logger.warn('Discord client disconnected');
// Clean up resources
cleanupConnections();
});
API Errors
Handle Discord API errors:
async function handleDiscordAPIError(error: DiscordAPIError) {
switch (error.code) {
case 10008: // Unknown Message
logger.debug('Message not found, may have been deleted');
break;
case 50001: // Missing Access
logger.warn('Bot lacks access to channel');
break;
case 50013: // Missing Permissions
logger.warn('Bot missing required permissions');
break;
default:
logger.error('Discord API error:', error);
}
}
Integration Guide
Basic Setup
import { discordPlugin } from '@elizaos/plugin-discord';
import { AgentRuntime } from '@elizaos/core';
const runtime = new AgentRuntime({
plugins: [discordPlugin],
character: {
name: "MyBot",
clients: ["discord"],
settings: {
DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID,
DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN
}
}
});
await runtime.start();
Custom Actions
Add Discord-specific actions:
const customDiscordAction: Action = {
name: "DISCORD_CUSTOM",
description: "Custom Discord action",
async handler(runtime, message, state, options, callback) {
// Access Discord-specific context
const discordService = runtime.getService('discord') as DiscordService;
const channel = await discordService.client.channels.fetch(message.channelId);
// Perform Discord-specific operations
if (channel?.type === ChannelType.GuildText) {
await channel.setTopic('Updated by bot');
}
await callback({
text: "Custom action completed"
});
}
};
Event Handlers
Listen for Discord-specific events:
runtime.on(DiscordEventTypes.GUILD_MEMBER_ADD, async (event) => {
const { member, guild } = event;
// Welcome new members
const welcomeChannel = guild.channels.cache.find(
ch => ch.name === 'welcome'
);
if (welcomeChannel?.type === ChannelType.GuildText) {
await welcomeChannel.send(`Welcome ${member.user.username}!`);
}
});
Best Practices
-
Token Security
// Never hardcode tokens
const token = process.env.DISCORD_API_TOKEN;
if (!token) throw new Error('Discord token not configured');
-
Error Recovery
// Implement exponential backoff
async function retryWithBackoff(fn: Function, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000);
}
}
}
-
Resource Cleanup
// Clean up on shutdown
process.on('SIGINT', async () => {
await voiceManager.disconnectAll();
client.destroy();
process.exit(0);
});
-
Monitoring
// Track performance metrics
const metrics = {
messagesProcessed: 0,
averageResponseTime: 0,
activeVoiceConnections: 0
};
Debugging
Enable debug logging:
DEBUG=eliza:discord:* npm run start
Common debug points:
- Connection establishment
- Message processing pipeline
- Voice connection state
- Permission checks
- API rate limits
Support
For issues and questions: