@elizaos/plugin-discord 1.3.3 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,8 +14,9 @@ A Discord plugin implementation for ElizaOS, enabling rich integration with Disc
14
14
  - Media transcription capabilities
15
15
  - Channel state and voice state providers
16
16
  - Channel restriction support (limit bot to specific channels)
17
- - Robust permissions management for bot functionality
17
+ - Robust permissions management and audit event tracking
18
18
  - Event-driven architecture with comprehensive event handling
19
+ - History backfill with efficient batch processing
19
20
 
20
21
  ## Installation
21
22
 
@@ -34,83 +35,497 @@ The plugin requires the following environment variables:
34
35
  DISCORD_APPLICATION_ID=your_application_id
35
36
  DISCORD_API_TOKEN=your_api_token
36
37
 
37
- # Optional Settings
38
+ # Channel Restrictions (Optional)
38
39
  # Comma-separated list of Discord channel IDs to restrict the bot to.
39
- # If not set, the bot operates in all channels as usual.
40
+ # If not set, the bot operates in all channels.
41
+ # These channels cannot be removed via the leaveChannel action.
40
42
  CHANNEL_IDS=123456789012345678,987654321098765432
43
+
44
+ # Listen-only channels (Optional)
45
+ # Comma-separated list of channel IDs where the bot will only listen (not respond).
46
+ DISCORD_LISTEN_CHANNEL_IDS=123456789012345678
47
+
48
+ # Voice Channel (Optional)
49
+ # ID of the voice channel the bot should auto-join when scanning a guild.
50
+ # If not set, the bot selects based on member activity.
51
+ DISCORD_VOICE_CHANNEL_ID=123456789012345678
52
+
53
+ # Behavior Settings (Optional)
54
+ # If true, ignore messages from other bots (default: false)
55
+ DISCORD_SHOULD_IGNORE_BOT_MESSAGES=false
56
+
57
+ # If true, ignore direct messages (default: false)
58
+ DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES=false
59
+
60
+ # If true, only respond when explicitly @mentioned (default: false)
61
+ DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS=false
62
+
63
+ # Testing (Optional)
64
+ DISCORD_TEST_CHANNEL_ID=123456789012345678
65
+ ```
66
+
67
+ Settings can also be configured in your character file under `settings.discord`:
68
+
69
+ ```json
70
+ {
71
+ "settings": {
72
+ "discord": {
73
+ "shouldIgnoreBotMessages": false,
74
+ "shouldIgnoreDirectMessages": false,
75
+ "shouldRespondOnlyToMentions": false,
76
+ "allowedChannelIds": ["123456789012345678"]
77
+ }
78
+ }
79
+ }
41
80
  ```
42
81
 
43
82
  ## Usage
44
83
 
45
84
  ```json
46
- const character = {
47
- "plugins": [
48
- ...otherPlugins,
49
- "@elizaos/plugin-discord"
50
- ]
85
+ {
86
+ "plugins": ["@elizaos/plugin-discord"]
51
87
  }
52
88
  ```
53
89
 
90
+ ## Slash Command Permissions
91
+
92
+ The plugin uses a hybrid permission system that combines Discord's native features with ElizaOS-specific controls.
93
+
94
+ ### Permission Layers
95
+
96
+ Commands go through multiple permission checks in this order:
97
+
98
+ 1. **Discord Native Checks** (before interaction fires):
99
+ - User must have required Discord permissions
100
+ - Command must be available in the current context (guild vs DM)
101
+
102
+ 2. **ElizaOS Channel Whitelist** (if `CHANNEL_IDS` is set):
103
+ - Commands only work in whitelisted channels
104
+ - Unless command has `bypassChannelWhitelist: true`
105
+
106
+ 3. **Custom Validator** (if provided):
107
+ - Runs custom validation logic
108
+ - Full programmatic control
109
+
110
+ ### Registering Commands
111
+
112
+ ```typescript
113
+ import { PermissionFlagsBits } from 'discord.js';
114
+
115
+ // Simple command (works everywhere)
116
+ const helpCommand = {
117
+ name: 'help',
118
+ description: 'Show help information'
119
+ };
120
+
121
+ // Guild-only command
122
+ const serverInfoCommand = {
123
+ name: 'serverinfo',
124
+ description: 'Show server information',
125
+ guildOnly: true
126
+ };
127
+
128
+ // Requires Discord permission
129
+ const configCommand = {
130
+ name: 'config',
131
+ description: 'Configure bot settings',
132
+ requiredPermissions: PermissionFlagsBits.ManageGuild
133
+ };
134
+
135
+ // Bypasses channel whitelist
136
+ const utilityCommand = {
137
+ name: 'export',
138
+ description: 'Export data',
139
+ bypassChannelWhitelist: true
140
+ };
141
+
142
+ // Advanced: custom validation
143
+ const adminCommand = {
144
+ name: 'admin',
145
+ description: 'Admin-only command',
146
+ validator: async (interaction, runtime) => {
147
+ const adminIds = runtime.getSetting('ADMIN_USER_IDS')?.split(',') ?? [];
148
+ return adminIds.includes(interaction.user.id);
149
+ }
150
+ };
151
+
152
+ // Register commands
153
+ await runtime.emitEvent(['DISCORD_REGISTER_COMMANDS'], {
154
+ commands: [helpCommand, serverInfoCommand, configCommand, utilityCommand, adminCommand]
155
+ });
156
+ ```
157
+
158
+ ### Permission Options
159
+
160
+ | Option | Type | Description |
161
+ |--------|------|-------------|
162
+ | `guildOnly` | `boolean` | If true, command only works in guilds (not DMs) |
163
+ | `bypassChannelWhitelist` | `boolean` | If true, bypasses `CHANNEL_IDS` restrictions |
164
+ | `requiredPermissions` | `bigint \| string` | Discord permission bitfield (e.g., `PermissionFlagsBits.ManageGuild`) |
165
+ | `contexts` | `number[]` | Raw Discord contexts (0=Guild, 1=BotDM, 2=PrivateChannel) |
166
+ | `guildIds` | `string[]` | Register only in specific guilds (instant updates) |
167
+ | `validator` | `function` | Custom validation function for advanced logic |
168
+
169
+ ### Common Permission Values
170
+
171
+ From Discord.js `PermissionFlagsBits`:
172
+
173
+ - `ManageGuild` - Server settings
174
+ - `ManageChannels` - Channel management
175
+ - `ManageMessages` - Delete messages
176
+ - `BanMembers` - Ban users
177
+ - `KickMembers` - Kick users
178
+ - `ModerateMembers` - Timeout users
179
+ - `ManageRoles` - Role management
180
+ - `Administrator` - Full access
181
+
182
+ ### Design Rationale
183
+
184
+ **Why Hybrid Approach?**
185
+ - Discord's native permissions are powerful but limited to role-based access
186
+ - ElizaOS needs programmatic control for channel restrictions and custom logic
187
+ - Combining both gives developers the best of both worlds
188
+
189
+ **Why Simple Flags?**
190
+ - `guildOnly: true` is clearer than `contexts: [0]`
191
+ - Abstracts Discord API details
192
+ - Sensible defaults: zero config should "just work"
193
+
194
+ **Why Keep Channel Whitelist?**
195
+ - Discord's channel permissions are UI-based (Server Settings > Integrations)
196
+ - Programmatic control is better for developer experience
197
+ - Allows dynamic, runtime-based channel restrictions
198
+
54
199
  ### Available Actions
55
200
 
56
201
  The plugin provides the following actions:
57
202
 
58
- 1. **chatWithAttachments** - Handle messages with Discord attachments
59
- 2. **downloadMedia** - Download media files from Discord messages
60
- 3. **joinVoice** - Join a voice channel
61
- 4. **leaveVoice** - Leave a voice channel
62
- 5. **summarize** - Summarize conversation history
63
- 6. **transcribeMedia** - Transcribe audio/video media to text
203
+ | Action | Description |
204
+ |--------|-------------|
205
+ | **chatWithAttachments** | Handle messages with Discord attachments |
206
+ | **createPoll** | Create a poll in a Discord channel |
207
+ | **downloadMedia** | Download media files from Discord messages |
208
+ | **getUserInfo** | Get information about a Discord user |
209
+ | **joinVoice** | Join a voice channel |
210
+ | **leaveVoice** | Leave a voice channel |
211
+ | **listChannels** | List channels in a Discord server |
212
+ | **pinMessage** | Pin a message in a channel |
213
+ | **reactToMessage** | Add a reaction to a message |
214
+ | **readChannel** | Read messages from a channel |
215
+ | **searchMessages** | Search for messages in a channel |
216
+ | **sendDM** | Send a direct message to a user |
217
+ | **serverInfo** | Get information about the current server |
218
+ | **summarize** | Summarize conversation history |
219
+ | **transcribeMedia** | Transcribe audio/video media to text |
220
+ | **unpinMessage** | Unpin a message from a channel |
64
221
 
65
222
  ### Providers
66
223
 
67
224
  The plugin includes two state providers:
68
225
 
69
226
  1. **channelStateProvider** - Provides state information about Discord channels
70
- 2. **voiceStateProvider** - Provides state information about voice channels
227
+ 2. **voiceStateProvider** - Provides state information about voice channels and connection status
71
228
 
72
229
  ### Event Types
73
230
 
74
231
  The plugin emits the following Discord-specific events:
75
232
 
76
- - `GUILD_MEMBER_ADD` - When a new member joins a guild
77
- - `GUILD_CREATE` - When the bot joins a guild
78
- - `MESSAGE_CREATE` - When a message is created
79
- - `INTERACTION_CREATE` - When an interaction is created
80
- - `REACTION_RECEIVED` - When a reaction is added to a message
233
+ | Event | Description |
234
+ |-------|-------------|
235
+ | `DISCORD_MESSAGE_RECEIVED` | When a message is received |
236
+ | `DISCORD_MESSAGE_SENT` | When a message is sent |
237
+ | `DISCORD_SLASH_COMMAND` | When a slash command is invoked |
238
+ | `DISCORD_MODAL_SUBMIT` | When a modal form is submitted |
239
+ | `DISCORD_REACTION_RECEIVED` | When a reaction is added to a message |
240
+ | `DISCORD_REACTION_REMOVED` | When a reaction is removed from a message |
241
+ | `DISCORD_WORLD_JOINED` | When the bot joins a guild |
242
+ | `DISCORD_SERVER_CONNECTED` | When connected to a server |
243
+ | `DISCORD_USER_JOINED` | When a user joins a guild |
244
+ | `DISCORD_USER_LEFT` | When a user leaves a guild |
245
+ | `DISCORD_VOICE_STATE_CHANGED` | When voice state changes |
246
+ | `DISCORD_CHANNEL_PERMISSIONS_CHANGED` | When channel permissions change |
247
+ | `DISCORD_ROLE_PERMISSIONS_CHANGED` | When role permissions change |
248
+ | `DISCORD_MEMBER_ROLES_CHANGED` | When a member's roles change |
249
+ | `DISCORD_ROLE_CREATED` | When a role is created |
250
+ | `DISCORD_ROLE_DELETED` | When a role is deleted |
81
251
 
82
252
  ## Key Components
83
253
 
84
- 1. **DiscordService**
85
- - Main service class that extends ElizaOS Service
86
- - Handles authentication and session management
87
- - Manages Discord client connection
88
- - Processes events and interactions
89
-
90
- 2. **MessageManager**
91
- - Processes incoming messages and responses
92
- - Handles attachments and media files
93
- - Supports message formatting and templating
94
- - Manages conversation context
95
-
96
- 3. **VoiceManager**
97
- - Manages voice channel interactions
98
- - Handles joining and leaving voice channels
99
- - Processes voice events and audio streams
100
- - Integrates with transcription services
101
-
102
- 4. **Attachment Handler**
103
- - Downloads and processes Discord attachments
104
- - Supports various media types
105
- - Integrates with media transcription
254
+ ### DiscordService
255
+
256
+ Main service class that extends ElizaOS Service:
257
+ - Handles authentication and session management
258
+ - Manages Discord client connection
259
+ - Processes events and interactions
260
+ - Supports channel history backfill with efficient batch processing
261
+
262
+ ### MessageManager
263
+
264
+ - Processes incoming messages and responses
265
+ - Handles attachments and media files
266
+ - Supports message formatting and templating
267
+ - Manages conversation context
268
+
269
+ ### VoiceManager
270
+
271
+ - Manages voice channel interactions
272
+ - Handles joining and leaving voice channels
273
+ - Processes voice events and audio streams
274
+ - Integrates with transcription services
275
+
276
+ ### Attachment Handler
277
+
278
+ - Downloads and processes Discord attachments
279
+ - Supports various media types
280
+ - Integrates with media transcription
281
+
282
+ ## Developer Guide
283
+
284
+ ### Custom Slash Commands
285
+
286
+ Register slash commands via the `DISCORD_REGISTER_COMMANDS` event, then listen for interactions:
287
+
288
+ ```typescript
289
+ // Register custom slash commands
290
+ await runtime.emitEvent(['DISCORD_REGISTER_COMMANDS'], {
291
+ commands: [
292
+ {
293
+ name: 'mycommand',
294
+ description: 'My custom command',
295
+ options: [
296
+ {
297
+ name: 'input',
298
+ description: 'User input',
299
+ type: 3, // STRING type
300
+ required: true,
301
+ },
302
+ ],
303
+ },
304
+ {
305
+ name: 'serverinfo',
306
+ description: 'Get server information',
307
+ guildOnly: true, // Only works in guilds, not DMs
308
+ },
309
+ ],
310
+ });
311
+
312
+ // Listen for slash command events to handle the interaction
313
+ runtime.registerEvent({
314
+ name: 'DISCORD_SLASH_COMMAND',
315
+ handler: async (payload) => {
316
+ const { interaction, client, commands } = payload;
317
+
318
+ if (interaction.commandName === 'mycommand') {
319
+ const input = interaction.options.getString('input');
320
+ await interaction.reply(`You said: ${input}`);
321
+ }
322
+ },
323
+ });
324
+ ```
325
+
326
+ ### Building on the Listen System
327
+
328
+ The `DISCORD_LISTEN_CHANNEL_IDS` setting creates "listen-only" channels where the bot receives messages but doesn't respond. This is useful for:
329
+
330
+ - **Monitoring channels** - Track activity without interrupting conversations
331
+ - **Data collection** - Gather messages for analysis or training
332
+ - **Conditional responses** - Build custom logic that decides when to respond
333
+
334
+ ```typescript
335
+ // Check if a channel is listen-only
336
+ const listenChannels = runtime.getSetting('DISCORD_LISTEN_CHANNEL_IDS');
337
+ const listenChannelIds = listenChannels?.split(',').map(s => s.trim()) || [];
338
+
339
+ runtime.registerEvent({
340
+ name: 'DISCORD_MESSAGE_RECEIVED',
341
+ handler: async (payload) => {
342
+ const { message } = payload;
343
+ const channelId = message.content.channelId;
344
+
345
+ if (listenChannelIds.includes(channelId)) {
346
+ // This is a listen-only channel - process without responding
347
+ await processMessageSilently(message);
348
+ }
349
+ },
350
+ });
351
+ ```
352
+
353
+ ### Handling Modal and Component Interactions
354
+
355
+ Modal submits and message components (buttons, select menus) bypass channel whitelists to support multi-step UI flows:
356
+
357
+ ```typescript
358
+ // Listen for modal submissions
359
+ runtime.registerEvent({
360
+ name: 'DISCORD_MODAL_SUBMIT',
361
+ handler: async (payload) => {
362
+ const { interaction } = payload;
363
+ const fieldValue = interaction.fields.getTextInputValue('myField');
364
+ await interaction.reply(`Received: ${fieldValue}`);
365
+ },
366
+ });
367
+ ```
368
+
369
+ ### Permission Audit System
370
+
371
+ The plugin includes a comprehensive permission audit system that tracks all permission changes with full audit log integration. This is useful for:
372
+
373
+ - **Security monitoring** - Detect unauthorized permission escalations
374
+ - **Compliance logging** - Maintain records of who changed what and when
375
+ - **Bot self-protection** - Detect when the bot's permissions are modified
376
+
377
+ #### Event Payloads
378
+
379
+ **DISCORD_CHANNEL_PERMISSIONS_CHANGED** - When channel overwrites change:
380
+
381
+ ```typescript
382
+ interface ChannelPermissionsChangedPayload {
383
+ runtime: IAgentRuntime;
384
+ guild: { id: string; name: string };
385
+ channel: { id: string; name: string };
386
+ target: { type: 'role' | 'user'; id: string; name: string };
387
+ action: 'CREATE' | 'UPDATE' | 'DELETE';
388
+ changes: Array<{
389
+ permission: string; // e.g., 'ManageMessages', 'Administrator'
390
+ oldState: 'ALLOW' | 'DENY' | 'NEUTRAL';
391
+ newState: 'ALLOW' | 'DENY' | 'NEUTRAL';
392
+ }>;
393
+ audit: { executorId: string; executorTag: string; reason: string | null } | null;
394
+ }
395
+ ```
396
+
397
+ **DISCORD_ROLE_PERMISSIONS_CHANGED** - When role permissions change:
398
+
399
+ ```typescript
400
+ interface RolePermissionsChangedPayload {
401
+ runtime: IAgentRuntime;
402
+ guild: { id: string; name: string };
403
+ role: { id: string; name: string };
404
+ changes: PermissionDiff[];
405
+ audit: AuditInfo | null;
406
+ }
407
+ ```
408
+
409
+ **DISCORD_MEMBER_ROLES_CHANGED** - When a member's roles change:
410
+
411
+ ```typescript
412
+ interface MemberRolesChangedPayload {
413
+ runtime: IAgentRuntime;
414
+ guild: { id: string; name: string };
415
+ member: { id: string; tag: string };
416
+ added: Array<{ id: string; name: string; permissions: string[] }>;
417
+ removed: Array<{ id: string; name: string; permissions: string[] }>;
418
+ audit: AuditInfo | null;
419
+ }
420
+ ```
421
+
422
+ **DISCORD_ROLE_CREATED / DISCORD_ROLE_DELETED** - Role lifecycle:
423
+
424
+ ```typescript
425
+ interface RoleLifecyclePayload {
426
+ runtime: IAgentRuntime;
427
+ guild: { id: string; name: string };
428
+ role: { id: string; name: string; permissions: string[] };
429
+ audit: AuditInfo | null;
430
+ }
431
+ ```
432
+
433
+ #### Example: Security Monitoring
434
+
435
+ ```typescript
436
+ import { DiscordEventTypes } from '@elizaos/plugin-discord';
437
+
438
+ // Alert on dangerous permission grants
439
+ runtime.registerEvent({
440
+ name: DiscordEventTypes.CHANNEL_PERMISSIONS_CHANGED,
441
+ handler: async (payload) => {
442
+ const dangerousPerms = ['Administrator', 'ManageGuild', 'ManageRoles'];
443
+
444
+ for (const change of payload.changes) {
445
+ if (dangerousPerms.includes(change.permission) && change.newState === 'ALLOW') {
446
+ console.warn(`⚠️ Dangerous permission granted!`, {
447
+ channel: payload.channel.name,
448
+ target: payload.target.name,
449
+ permission: change.permission,
450
+ grantedBy: payload.audit?.executorTag || 'Unknown',
451
+ });
452
+ }
453
+ }
454
+ },
455
+ });
456
+
457
+ // Track role escalations
458
+ runtime.registerEvent({
459
+ name: DiscordEventTypes.MEMBER_ROLES_CHANGED,
460
+ handler: async (payload) => {
461
+ const adminRoles = payload.added.filter(r =>
462
+ r.permissions.includes('Administrator')
463
+ );
464
+
465
+ if (adminRoles.length > 0) {
466
+ console.warn(`⚠️ Admin role granted to ${payload.member.tag}`, {
467
+ roles: adminRoles.map(r => r.name),
468
+ grantedBy: payload.audit?.executorTag || 'Unknown',
469
+ });
470
+ }
471
+ },
472
+ });
473
+
474
+ // Log all role creations
475
+ runtime.registerEvent({
476
+ name: DiscordEventTypes.ROLE_CREATED,
477
+ handler: async (payload) => {
478
+ console.log(`New role created: ${payload.role.name}`, {
479
+ permissions: payload.role.permissions,
480
+ createdBy: payload.audit?.executorTag || 'Unknown',
481
+ });
482
+ },
483
+ });
484
+ ```
485
+
486
+ #### Bot Self-Protection
487
+
488
+ Monitor when the bot's own permissions change:
489
+
490
+ ```typescript
491
+ runtime.registerEvent({
492
+ name: DiscordEventTypes.MEMBER_ROLES_CHANGED,
493
+ handler: async (payload) => {
494
+ const botId = runtime.getSetting('DISCORD_APPLICATION_ID');
495
+
496
+ if (payload.member.id === botId && payload.removed.length > 0) {
497
+ console.error(`🚨 Bot roles removed!`, {
498
+ removed: payload.removed.map(r => r.name),
499
+ by: payload.audit?.executorTag || 'Unknown',
500
+ });
501
+ // Could trigger alerts, notifications, etc.
502
+ }
503
+ },
504
+ });
505
+ ```
506
+
507
+ ## Cross-Core Compatibility
508
+
509
+ This plugin includes a compatibility layer (`compat.ts`) that allows it to work with both old and new versions of `@elizaos/core`. The compatibility layer:
510
+
511
+ - Automatically handles `serverId` vs `messageServerId` differences
512
+ - Uses a runtime proxy to intercept and adapt API calls
513
+ - Requires no changes to existing code
514
+
515
+ When migrating to a new core version, see the comments in `compat.ts` for removal instructions.
106
516
 
107
517
  ## Testing
108
518
 
109
- The plugin includes a test suite (`DiscordTestSuite`) for validating functionality.
519
+ The plugin includes a test suite for validating functionality:
520
+
521
+ ```bash
522
+ bun run test
523
+ ```
110
524
 
111
525
  ## Notes
112
526
 
113
- - Ensure that your `.env` file includes the required `DISCORD_API_TOKEN` for proper functionality
114
- - The bot requires appropriate Discord permissions to function correctly (send messages, connect to voice channels, etc.)
527
+ - Ensure that your `.env` file includes the required `DISCORD_API_TOKEN`
528
+ - The bot requires appropriate Discord permissions (send messages, connect to voice, etc.)
115
529
  - If no token is provided, the plugin will load but remain non-functional with appropriate warnings
116
530
  - The plugin uses Discord.js v14.18.0 with comprehensive intent support
531
+ - Slash commands and modal/component interactions bypass channel whitelists