@@ -22,7 +22,7 @@ import { MessageManager } from "./messages.ts";
22
22
import channelStateProvider from "./providers/channelState.ts" ;
23
23
import voiceStateProvider from "./providers/voiceState.ts" ;
24
24
import { VoiceManager } from "./voice.ts" ;
25
- import { validateDiscordConfig } from "./enviroment.ts " ;
25
+ import { PermissionsBitField } from "discord.js " ;
26
26
27
27
export class DiscordClient extends EventEmitter {
28
28
apiToken : string ;
@@ -34,6 +34,7 @@ export class DiscordClient extends EventEmitter {
34
34
35
35
constructor ( runtime : IAgentRuntime ) {
36
36
super ( ) ;
37
+
37
38
this . apiToken = runtime . getSetting ( "DISCORD_API_TOKEN" ) as string ;
38
39
this . client = new Client ( {
39
40
intents : [
@@ -112,85 +113,173 @@ export class DiscordClient extends EventEmitter {
112
113
113
114
private async onClientReady ( readyClient : { user : { tag : any ; id : any } } ) {
114
115
elizaLogger . success ( `Logged in as ${ readyClient . user ?. tag } ` ) ;
116
+
117
+ // Register slash commands
118
+ const commands = [
119
+ {
120
+ name : "joinchannel" ,
121
+ description : "Join a voice channel" ,
122
+ options : [
123
+ {
124
+ name : "channel" ,
125
+ type : 7 , // CHANNEL type
126
+ description : "The voice channel to join" ,
127
+ required : true ,
128
+ channel_types : [ 2 ] , // GuildVoice type
129
+ } ,
130
+ ] ,
131
+ } ,
132
+ {
133
+ name : "leavechannel" ,
134
+ description : "Leave the current voice channel" ,
135
+ } ,
136
+ ] ;
137
+
138
+ try {
139
+ await this . client . application ?. commands . set ( commands ) ;
140
+ elizaLogger . success ( "Slash commands registered" ) ;
141
+ } catch ( error ) {
142
+ console . error ( "Error registering slash commands:" , error ) ;
143
+ }
144
+
145
+ // Required permissions for the bot
146
+ const requiredPermissions = [
147
+ // Text Permissions
148
+ PermissionsBitField . Flags . ViewChannel ,
149
+ PermissionsBitField . Flags . SendMessages ,
150
+ PermissionsBitField . Flags . SendMessagesInThreads ,
151
+ PermissionsBitField . Flags . CreatePrivateThreads ,
152
+ PermissionsBitField . Flags . CreatePublicThreads ,
153
+ PermissionsBitField . Flags . EmbedLinks ,
154
+ PermissionsBitField . Flags . AttachFiles ,
155
+ PermissionsBitField . Flags . AddReactions ,
156
+ PermissionsBitField . Flags . UseExternalEmojis ,
157
+ PermissionsBitField . Flags . UseExternalStickers ,
158
+ PermissionsBitField . Flags . MentionEveryone ,
159
+ PermissionsBitField . Flags . ManageMessages ,
160
+ PermissionsBitField . Flags . ReadMessageHistory ,
161
+ // Voice Permissions
162
+ PermissionsBitField . Flags . Connect ,
163
+ PermissionsBitField . Flags . Speak ,
164
+ PermissionsBitField . Flags . UseVAD ,
165
+ PermissionsBitField . Flags . PrioritySpeaker ,
166
+ ] . reduce ( ( a , b ) => a | b , 0n ) ;
167
+
115
168
elizaLogger . success ( "Use this URL to add the bot to your server:" ) ;
116
169
elizaLogger . success (
117
- `https://discord.com/api/oauth2/authorize?client_id=${ readyClient . user ?. id } &permissions=0 &scope=bot%20applications.commands`
170
+ `https://discord.com/api/oauth2/authorize?client_id=${ readyClient . user ?. id } &permissions=${ requiredPermissions } &scope=bot%20applications.commands`
118
171
) ;
119
172
await this . onReady ( ) ;
120
173
}
121
174
122
175
async handleReactionAdd ( reaction : MessageReaction , user : User ) {
123
- elizaLogger . log ( "Reaction added" ) ;
124
- // if (user.bot) return;
125
-
126
- let emoji = reaction . emoji . name ;
127
- if ( ! emoji && reaction . emoji . id ) {
128
- emoji = `<:${ reaction . emoji . name } :${ reaction . emoji . id } >` ;
129
- }
176
+ try {
177
+ elizaLogger . log ( "Reaction added" ) ;
130
178
131
- // Fetch the full message if it's a partial
132
- if ( reaction . partial ) {
133
- try {
134
- await reaction . fetch ( ) ;
135
- } catch ( error ) {
136
- console . error (
137
- "Something went wrong when fetching the message:" ,
138
- error
139
- ) ;
179
+ // Early returns
180
+ if ( ! reaction || ! user ) {
181
+ elizaLogger . warn ( "Invalid reaction or user" ) ;
140
182
return ;
141
183
}
142
- }
143
184
144
- const messageContent = reaction . message . content ;
145
- const truncatedContent =
146
- messageContent . length > 100
147
- ? messageContent . substring ( 0 , 100 ) + "..."
148
- : messageContent ;
185
+ // Get emoji info
186
+ let emoji = reaction . emoji . name ;
187
+ if ( ! emoji && reaction . emoji . id ) {
188
+ emoji = `<: ${ reaction . emoji . name } : ${ reaction . emoji . id } >` ;
189
+ }
149
190
150
- const reactionMessage = `*<${ emoji } >: "${ truncatedContent } "*` ;
191
+ // Fetch full message if partial
192
+ if ( reaction . partial ) {
193
+ try {
194
+ await reaction . fetch ( ) ;
195
+ } catch ( error ) {
196
+ elizaLogger . error (
197
+ "Failed to fetch partial reaction:" ,
198
+ error
199
+ ) ;
200
+ return ;
201
+ }
202
+ }
151
203
152
- const roomId = stringToUuid (
153
- reaction . message . channel . id + "-" + this . runtime . agentId
154
- ) ;
155
- const userIdUUID = stringToUuid ( user . id + "-" + this . runtime . agentId ) ;
204
+ // Generate IDs with timestamp to ensure uniqueness
205
+ const timestamp = Date . now ( ) ;
206
+ const roomId = stringToUuid (
207
+ `${ reaction . message . channel . id } -${ this . runtime . agentId } `
208
+ ) ;
209
+ const userIdUUID = stringToUuid (
210
+ `${ user . id } -${ this . runtime . agentId } `
211
+ ) ;
212
+ const reactionUUID = stringToUuid (
213
+ `${ reaction . message . id } -${ user . id } -${ emoji } -${ timestamp } -${ this . runtime . agentId } `
214
+ ) ;
215
+
216
+ // Validate IDs
217
+ if ( ! userIdUUID || ! roomId ) {
218
+ elizaLogger . error ( "Invalid user ID or room ID" , {
219
+ userIdUUID,
220
+ roomId,
221
+ } ) ;
222
+ return ;
223
+ }
156
224
157
- // Generate a unique UUID for the reaction
158
- const reactionUUID = stringToUuid (
159
- `${ reaction . message . id } -${ user . id } -${ emoji } -${ this . runtime . agentId } `
160
- ) ;
225
+ // Process message content
226
+ const messageContent = reaction . message . content || "" ;
227
+ const truncatedContent =
228
+ messageContent . length > 100
229
+ ? `${ messageContent . substring ( 0 , 100 ) } ...`
230
+ : messageContent ;
231
+ const reactionMessage = `*<${ emoji } >: "${ truncatedContent } "*` ;
232
+
233
+ // Get user info
234
+ const userName = reaction . message . author ?. username || "unknown" ;
235
+ const name = reaction . message . author ?. displayName || userName ;
236
+
237
+ // Ensure connection
238
+ await this . runtime . ensureConnection (
239
+ userIdUUID ,
240
+ roomId ,
241
+ userName ,
242
+ name ,
243
+ "discord"
244
+ ) ;
245
+
246
+ // Create memory with retry logic
247
+ const memory = {
248
+ id : reactionUUID ,
249
+ userId : userIdUUID ,
250
+ agentId : this . runtime . agentId ,
251
+ content : {
252
+ text : reactionMessage ,
253
+ source : "discord" ,
254
+ inReplyTo : stringToUuid (
255
+ `${ reaction . message . id } -${ this . runtime . agentId } `
256
+ ) ,
257
+ } ,
258
+ roomId,
259
+ createdAt : timestamp ,
260
+ embedding : getEmbeddingZeroVector ( this . runtime ) ,
261
+ } ;
161
262
162
- // ensure the user id and room id are valid
163
- if ( ! userIdUUID || ! roomId ) {
164
- console . error ( "Invalid user id or room id" ) ;
165
- return ;
263
+ try {
264
+ await this . runtime . messageManager . createMemory ( memory ) ;
265
+ elizaLogger . debug ( "Reaction memory created" , {
266
+ reactionId : reactionUUID ,
267
+ emoji,
268
+ userId : user . id ,
269
+ } ) ;
270
+ } catch ( error ) {
271
+ if ( error . code === "23505" ) {
272
+ // Duplicate key error
273
+ elizaLogger . warn ( "Duplicate reaction memory, skipping" , {
274
+ reactionId : reactionUUID ,
275
+ } ) ;
276
+ return ;
277
+ }
278
+ throw error ; // Re-throw other errors
279
+ }
280
+ } catch ( error ) {
281
+ elizaLogger . error ( "Error handling reaction:" , error ) ;
166
282
}
167
- const userName = reaction . message . author . username ;
168
- const name = reaction . message . author . displayName ;
169
-
170
- await this . runtime . ensureConnection (
171
- userIdUUID ,
172
- roomId ,
173
- userName ,
174
- name ,
175
- "discord"
176
- ) ;
177
-
178
- // Save the reaction as a message
179
- await this . runtime . messageManager . createMemory ( {
180
- id : reactionUUID , // This is the ID of the reaction message
181
- userId : userIdUUID ,
182
- agentId : this . runtime . agentId ,
183
- content : {
184
- text : reactionMessage ,
185
- source : "discord" ,
186
- inReplyTo : stringToUuid (
187
- reaction . message . id + "-" + this . runtime . agentId
188
- ) , // This is the ID of the original message
189
- } ,
190
- roomId,
191
- createdAt : Date . now ( ) ,
192
- embedding : getEmbeddingZeroVector ( this . runtime ) ,
193
- } ) ;
194
283
}
195
284
196
285
async handleReactionRemove ( reaction : MessageReaction , user : User ) {
@@ -298,12 +387,8 @@ export function startDiscord(runtime: IAgentRuntime) {
298
387
}
299
388
300
389
export const DiscordClientInterface : ElizaClient = {
301
- start : async ( runtime : IAgentRuntime ) => {
302
- await validateDiscordConfig ( runtime ) ;
303
-
304
- return new DiscordClient ( runtime ) ;
305
- } ,
306
- stop : async ( _runtime : IAgentRuntime ) => {
390
+ start : async ( runtime : IAgentRuntime ) => new DiscordClient ( runtime ) ,
391
+ stop : async ( runtime : IAgentRuntime ) => {
307
392
console . warn ( "Discord client does not support stopping yet" ) ;
308
393
} ,
309
- } ;
394
+ } ;
0 commit comments