@elizaos/plugin-discord 1.0.0-alpha.0

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/dist/index.js ADDED
@@ -0,0 +1,3946 @@
1
+ // src/index.ts
2
+ import {
3
+ ChannelType as ChannelType10,
4
+ createUniqueUuid as createUniqueUuid6,
5
+ logger as logger7,
6
+ Role,
7
+ Service
8
+ } from "@elizaos/core";
9
+ import {
10
+ ChannelType as DiscordChannelType4,
11
+ Client as DiscordJsClient,
12
+ Events as Events2,
13
+ GatewayIntentBits,
14
+ Partials,
15
+ PermissionsBitField as PermissionsBitField2
16
+ } from "discord.js";
17
+
18
+ // src/actions/chatWithAttachments.ts
19
+ import {
20
+ ChannelType,
21
+ composePrompt,
22
+ ModelTypes,
23
+ parseJSONObjectFromText,
24
+ trimTokens
25
+ } from "@elizaos/core";
26
+ import * as fs from "node:fs";
27
+ var summarizationTemplate = `# Summarized so far (we are adding to this)
28
+ {{currentSummary}}
29
+
30
+ # Current attachments we are summarizing
31
+ {{attachmentsWithText}}
32
+
33
+ Summarization objective: {{objective}}
34
+
35
+ # Instructions: Summarize the attachments. Return the summary. Do not acknowledge this request, just summarize and continue the existing summary if there is one. Capture any important details based on the objective. Only respond with the new summary text.`;
36
+ var attachmentIdsTemplate = `# Messages we are summarizing
37
+ {{recentMessages}}
38
+
39
+ # Instructions: {{senderName}} is requesting a summary of specific attachments. Your goal is to determine their objective, along with the list of attachment IDs to summarize.
40
+ The "objective" is a detailed description of what the user wants to summarize based on the conversation.
41
+ The "attachmentIds" is an array of attachment IDs that the user wants to summarize. If not specified, default to including all attachments from the conversation.
42
+
43
+ Your response must be formatted as a JSON block with this structure:
44
+ \`\`\`json
45
+ {
46
+ "objective": "<What the user wants to summarize>",
47
+ "attachmentIds": ["<Attachment ID 1>", "<Attachment ID 2>", ...]
48
+ }
49
+ \`\`\`
50
+ `;
51
+ var getAttachmentIds = async (runtime, _message, state) => {
52
+ const prompt = composePrompt({
53
+ state,
54
+ template: attachmentIdsTemplate
55
+ });
56
+ for (let i = 0; i < 5; i++) {
57
+ const response = await runtime.useModel(ModelTypes.TEXT_SMALL, {
58
+ prompt
59
+ });
60
+ console.log("response", response);
61
+ const parsedResponse = parseJSONObjectFromText(response);
62
+ if (parsedResponse?.objective && parsedResponse?.attachmentIds) {
63
+ return parsedResponse;
64
+ }
65
+ }
66
+ return null;
67
+ };
68
+ var summarizeAction = {
69
+ name: "CHAT_WITH_ATTACHMENTS",
70
+ similes: [
71
+ "CHAT_WITH_ATTACHMENT",
72
+ "SUMMARIZE_FILES",
73
+ "SUMMARIZE_FILE",
74
+ "SUMMARIZE_ATACHMENT",
75
+ "CHAT_WITH_PDF",
76
+ "ATTACHMENT_SUMMARY",
77
+ "RECAP_ATTACHMENTS",
78
+ "SUMMARIZE_FILE",
79
+ "SUMMARIZE_VIDEO",
80
+ "SUMMARIZE_AUDIO",
81
+ "SUMMARIZE_IMAGE",
82
+ "SUMMARIZE_DOCUMENT",
83
+ "SUMMARIZE_LINK",
84
+ "ATTACHMENT_SUMMARY",
85
+ "FILE_SUMMARY"
86
+ ],
87
+ description: "Answer a user request informed by specific attachments based on their IDs. If a user asks to chat with a PDF, or wants more specific information about a link or video or anything else they've attached, this is the action to use.",
88
+ validate: async (_runtime, message, _state) => {
89
+ const room = await _runtime.getDatabaseAdapter().getRoom(message.roomId);
90
+ if (room?.type !== ChannelType.GROUP) {
91
+ return false;
92
+ }
93
+ const keywords = [
94
+ "attachment",
95
+ "summary",
96
+ "summarize",
97
+ "research",
98
+ "pdf",
99
+ "video",
100
+ "audio",
101
+ "image",
102
+ "document",
103
+ "link",
104
+ "file",
105
+ "attachment",
106
+ "summarize",
107
+ "code",
108
+ "report",
109
+ "write",
110
+ "details",
111
+ "information",
112
+ "talk",
113
+ "chat",
114
+ "read",
115
+ "listen",
116
+ "watch"
117
+ ];
118
+ return keywords.some(
119
+ (keyword) => message.content.text.toLowerCase().includes(keyword.toLowerCase())
120
+ );
121
+ },
122
+ handler: async (runtime, message, state, _options, callback) => {
123
+ const callbackData = {
124
+ text: "",
125
+ // fill in later
126
+ actions: ["CHAT_WITH_ATTACHMENTS_RESPONSE"],
127
+ source: message.content.source,
128
+ attachments: []
129
+ };
130
+ const attachmentData = await getAttachmentIds(runtime, message, state);
131
+ if (!attachmentData) {
132
+ console.error("Couldn't get attachment IDs from message");
133
+ await runtime.getMemoryManager("messages").createMemory({
134
+ entityId: message.entityId,
135
+ agentId: message.agentId,
136
+ roomId: message.roomId,
137
+ content: {
138
+ source: message.content.source,
139
+ thought: "I tried to chat with attachments but I couldn't get attachment IDs",
140
+ actions: ["CHAT_WITH_ATTACHMENTS_FAILED"]
141
+ },
142
+ metadata: {
143
+ type: "CHAT_WITH_ATTACHMENTS"
144
+ }
145
+ });
146
+ return;
147
+ }
148
+ const { objective, attachmentIds } = attachmentData;
149
+ const attachments = state.data.recentMessages.filter(
150
+ (msg) => msg.content.attachments && msg.content.attachments.length > 0
151
+ ).flatMap((msg) => msg.content.attachments).filter(
152
+ (attachment) => attachmentIds.map((attch) => attch.toLowerCase().slice(0, 5)).includes(attachment.id.toLowerCase().slice(0, 5)) || // or check the other way
153
+ attachmentIds.some((id) => {
154
+ const attachmentId = id.toLowerCase().slice(0, 5);
155
+ return attachment.id.toLowerCase().includes(attachmentId);
156
+ })
157
+ );
158
+ const attachmentsWithText = attachments.map((attachment) => `# ${attachment.title}
159
+ ${attachment.text}`).join("\n\n");
160
+ let currentSummary = "";
161
+ const chunkSize = 8192;
162
+ state.values.attachmentsWithText = attachmentsWithText;
163
+ state.values.objective = objective;
164
+ const template = await trimTokens(
165
+ summarizationTemplate,
166
+ chunkSize,
167
+ runtime
168
+ );
169
+ const prompt = composePrompt({
170
+ state,
171
+ // make sure it fits, we can pad the tokens a bit
172
+ // Get the model's tokenizer based on the current model being used
173
+ template
174
+ });
175
+ const summary = await runtime.useModel(ModelTypes.TEXT_SMALL, {
176
+ prompt
177
+ });
178
+ currentSummary = `${currentSummary}
179
+ ${summary}`;
180
+ if (!currentSummary) {
181
+ console.error("No summary found, that's not good!");
182
+ await runtime.getMemoryManager("messages").createMemory({
183
+ entityId: message.entityId,
184
+ agentId: message.agentId,
185
+ roomId: message.roomId,
186
+ content: {
187
+ source: message.content.source,
188
+ thought: "I tried to chat with attachments but I couldn't get a summary",
189
+ actions: ["CHAT_WITH_ATTACHMENTS_FAILED"]
190
+ },
191
+ metadata: {
192
+ type: "CHAT_WITH_ATTACHMENTS"
193
+ }
194
+ });
195
+ return;
196
+ }
197
+ callbackData.text = currentSummary.trim();
198
+ if (callbackData.text && (currentSummary.trim()?.split("\n").length < 4 || currentSummary.trim()?.split(" ").length < 100)) {
199
+ callbackData.text = `Here is the summary:
200
+ \`\`\`md
201
+ ${currentSummary.trim()}
202
+ \`\`\`
203
+ `;
204
+ await callback(callbackData);
205
+ } else if (currentSummary.trim()) {
206
+ const summaryDir = "cache";
207
+ const summaryFilename = `${summaryDir}/summary_${Date.now()}.md`;
208
+ try {
209
+ await fs.promises.mkdir(summaryDir, { recursive: true });
210
+ console.log("Creating summary file:", {
211
+ filename: summaryFilename,
212
+ summaryLength: currentSummary.length
213
+ });
214
+ await fs.promises.writeFile(summaryFilename, currentSummary, "utf8");
215
+ console.log("File written successfully");
216
+ await runtime.getDatabaseAdapter().setCache(summaryFilename, currentSummary);
217
+ console.log("Cache set operation completed");
218
+ await callback(
219
+ {
220
+ ...callbackData,
221
+ text: `I've attached the summary of the requested attachments as a text file.`
222
+ },
223
+ [summaryFilename]
224
+ );
225
+ console.log("Callback completed with summary file");
226
+ } catch (error) {
227
+ console.error("Error in file/cache process:", error);
228
+ throw error;
229
+ }
230
+ } else {
231
+ console.warn(
232
+ "Empty response from chat with attachments action, skipping"
233
+ );
234
+ }
235
+ return callbackData;
236
+ },
237
+ examples: [
238
+ [
239
+ {
240
+ name: "{{name1}}",
241
+ content: {
242
+ text: "Can you summarize the attachments b3e23, c4f67, and d5a89?"
243
+ }
244
+ },
245
+ {
246
+ name: "{{name2}}",
247
+ content: {
248
+ text: "Sure thing! I'll pull up those specific attachments and provide a summary of their content.",
249
+ actions: ["CHAT_WITH_ATTACHMENTS"]
250
+ }
251
+ }
252
+ ],
253
+ [
254
+ {
255
+ name: "{{name1}}",
256
+ content: {
257
+ text: "I need a technical summary of the PDFs I sent earlier - a1b2c3.pdf, d4e5f6.pdf, and g7h8i9.pdf"
258
+ }
259
+ },
260
+ {
261
+ name: "{{name2}}",
262
+ content: {
263
+ text: "I'll take a look at those specific PDF attachments and put together a technical summary for you. Give me a few minutes to review them.",
264
+ actions: ["CHAT_WITH_ATTACHMENTS"]
265
+ }
266
+ }
267
+ ],
268
+ [
269
+ {
270
+ name: "{{name1}}",
271
+ content: {
272
+ text: "Can you watch this video for me and tell me which parts you think are most relevant to the report I'm writing? (the one I attached in my last message)"
273
+ }
274
+ },
275
+ {
276
+ name: "{{name2}}",
277
+ content: {
278
+ text: "sure, no problem.",
279
+ actions: ["CHAT_WITH_ATTACHMENTS"]
280
+ }
281
+ }
282
+ ],
283
+ [
284
+ {
285
+ name: "{{name1}}",
286
+ content: {
287
+ text: "can you read my blog post and give me a detailed breakdown of the key points I made, and then suggest a handful of tweets to promote it?"
288
+ }
289
+ },
290
+ {
291
+ name: "{{name2}}",
292
+ content: {
293
+ text: "great idea, give me a minute",
294
+ actions: ["CHAT_WITH_ATTACHMENTS"]
295
+ }
296
+ }
297
+ ]
298
+ ]
299
+ };
300
+ var chatWithAttachments_default = summarizeAction;
301
+
302
+ // src/actions/downloadMedia.ts
303
+ import {
304
+ composePrompt as composePrompt2,
305
+ ModelTypes as ModelTypes2,
306
+ parseJSONObjectFromText as parseJSONObjectFromText2,
307
+ ServiceTypes
308
+ } from "@elizaos/core";
309
+ var mediaUrlTemplate = `# Messages we are searching for a media URL
310
+ {{recentMessages}}
311
+
312
+ # Instructions: {{senderName}} is requesting to download a specific media file (video or audio). Your goal is to determine the URL of the media they want to download.
313
+ The "mediaUrl" is the URL of the media file that the user wants downloaded. If not specified, return null.
314
+
315
+ Your response must be formatted as a JSON block with this structure:
316
+ \`\`\`json
317
+ {
318
+ "mediaUrl": "<Media URL>"
319
+ }
320
+ \`\`\`
321
+ `;
322
+ var getMediaUrl = async (runtime, _message, state) => {
323
+ const prompt = composePrompt2({
324
+ state,
325
+ template: mediaUrlTemplate
326
+ });
327
+ for (let i = 0; i < 5; i++) {
328
+ const response = await runtime.useModel(ModelTypes2.TEXT_SMALL, {
329
+ prompt
330
+ });
331
+ const parsedResponse = parseJSONObjectFromText2(response);
332
+ if (parsedResponse?.mediaUrl) {
333
+ return parsedResponse.mediaUrl;
334
+ }
335
+ }
336
+ return null;
337
+ };
338
+ var downloadMedia_default = {
339
+ name: "DOWNLOAD_MEDIA",
340
+ similes: [
341
+ "DOWNLOAD_VIDEO",
342
+ "DOWNLOAD_AUDIO",
343
+ "GET_MEDIA",
344
+ "DOWNLOAD_PODCAST",
345
+ "DOWNLOAD_YOUTUBE"
346
+ ],
347
+ description: "Downloads a video or audio file from a URL and attaches it to the response message.",
348
+ validate: async (_runtime, message, _state) => {
349
+ if (message.content.source !== "discord") {
350
+ return false;
351
+ }
352
+ },
353
+ handler: async (runtime, message, state, _options, callback) => {
354
+ const videoService = runtime.getService(ServiceTypes.VIDEO);
355
+ const mediaUrl = await getMediaUrl(runtime, message, state);
356
+ if (!mediaUrl) {
357
+ console.error("Couldn't get media URL from messages");
358
+ await runtime.getMemoryManager("messages").createMemory({
359
+ entityId: message.entityId,
360
+ agentId: message.agentId,
361
+ roomId: message.roomId,
362
+ content: {
363
+ source: "discord",
364
+ thought: `I couldn't find the media URL in the message`,
365
+ actions: ["DOWNLOAD_MEDIA_FAILED"]
366
+ },
367
+ metadata: {
368
+ type: "DOWNLOAD_MEDIA"
369
+ }
370
+ });
371
+ return;
372
+ }
373
+ const videoInfo = await videoService.fetchVideoInfo(mediaUrl);
374
+ const mediaPath = await videoService.downloadVideo(videoInfo);
375
+ const response = {
376
+ text: `I downloaded the video "${videoInfo.title}" and attached it below.`,
377
+ actions: ["DOWNLOAD_MEDIA_RESPONSE"],
378
+ source: message.content.source,
379
+ attachments: []
380
+ };
381
+ const maxRetries = 3;
382
+ let retries = 0;
383
+ while (retries < maxRetries) {
384
+ try {
385
+ await callback(
386
+ {
387
+ ...response
388
+ },
389
+ [mediaPath]
390
+ );
391
+ break;
392
+ } catch (error) {
393
+ retries++;
394
+ console.error(`Error sending message (attempt ${retries}):`, error);
395
+ if (retries === maxRetries) {
396
+ console.error(
397
+ "Max retries reached. Failed to send message with attachment."
398
+ );
399
+ break;
400
+ }
401
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
402
+ }
403
+ }
404
+ return response;
405
+ },
406
+ examples: [
407
+ [
408
+ {
409
+ name: "{{name1}}",
410
+ content: {
411
+ text: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
412
+ }
413
+ },
414
+ {
415
+ name: "{{name2}}",
416
+ content: {
417
+ text: "Downloading the YouTube video now, one sec",
418
+ actions: ["DOWNLOAD_MEDIA"]
419
+ }
420
+ }
421
+ ],
422
+ [
423
+ {
424
+ name: "{{name1}}",
425
+ content: {
426
+ text: "Can you grab this video for me? https://vimeo.com/123456789"
427
+ }
428
+ },
429
+ {
430
+ name: "{{name2}}",
431
+ content: {
432
+ text: "Sure thing, I'll download that Vimeo video for you",
433
+ actions: ["DOWNLOAD_MEDIA"]
434
+ }
435
+ }
436
+ ],
437
+ [
438
+ {
439
+ name: "{{name1}}",
440
+ content: {
441
+ text: "I need this video downloaded: https://www.youtube.com/watch?v=abcdefg"
442
+ }
443
+ },
444
+ {
445
+ name: "{{name2}}",
446
+ content: {
447
+ text: "No problem, I'm on it. I'll have that YouTube video downloaded in a jiffy",
448
+ actions: ["DOWNLOAD_MEDIA"]
449
+ }
450
+ }
451
+ ]
452
+ ]
453
+ };
454
+
455
+ // src/actions/summarizeConversation.ts
456
+ import {
457
+ composePrompt as composePrompt3,
458
+ getEntityDetails,
459
+ ModelTypes as ModelTypes3,
460
+ parseJSONObjectFromText as parseJSONObjectFromText3,
461
+ splitChunks,
462
+ trimTokens as trimTokens2
463
+ } from "@elizaos/core";
464
+ import * as fs2 from "node:fs";
465
+ var summarizationTemplate2 = `# Summarized so far (we are adding to this)
466
+ {{currentSummary}}
467
+
468
+ # Current conversation chunk we are summarizing (includes attachments)
469
+ {{memoriesWithAttachments}}
470
+
471
+ Summarization objective: {{objective}}
472
+
473
+ # Instructions: Summarize the conversation so far. Return the summary. Do not acknowledge this request, just summarize and continue the existing summary if there is one. Capture any important details to the objective. Only respond with the new summary text.
474
+ Your response should be extremely detailed and include any and all relevant information.`;
475
+ var dateRangeTemplate = `# Messages we are summarizing (the conversation is continued after this)
476
+ {{recentMessages}}
477
+
478
+ # Instructions: {{senderName}} is requesting a summary of the conversation. Your goal is to determine their objective, along with the range of dates that their request covers.
479
+ The "objective" is a detailed description of what the user wants to summarize based on the conversation. If they just ask for a general summary, you can either base it off the conversation if the summary range is very recent, or set the object to be general, like "a detailed summary of the conversation between all users".
480
+ The "start" and "end" are the range of dates that the user wants to summarize, relative to the current time. The start and end should be relative to the current time, and measured in seconds, minutes, hours and days. The format is "2 days ago" or "3 hours ago" or "4 minutes ago" or "5 seconds ago", i.e. "<integer> <unit> ago".
481
+ If you aren't sure, you can use a default range of "0 minutes ago" to "2 hours ago" or more. Better to err on the side of including too much than too little.
482
+
483
+ Your response must be formatted as a JSON block with this structure:
484
+ \`\`\`json
485
+ {
486
+ "objective": "<What the user wants to summarize>",
487
+ "start": "0 minutes ago",
488
+ "end": "2 hours ago"
489
+ }
490
+ \`\`\`
491
+ `;
492
+ var getDateRange = async (runtime, _message, state) => {
493
+ const prompt = composePrompt3({
494
+ state,
495
+ template: dateRangeTemplate
496
+ });
497
+ for (let i = 0; i < 5; i++) {
498
+ const response = await runtime.useModel(ModelTypes3.TEXT_SMALL, {
499
+ prompt
500
+ });
501
+ console.log("response", response);
502
+ const parsedResponse = parseJSONObjectFromText3(response);
503
+ if (parsedResponse) {
504
+ if (parsedResponse.objective && parsedResponse.start && parsedResponse.end) {
505
+ const startIntegerString = parsedResponse.start.match(
506
+ /\d+/
507
+ )?.[0];
508
+ const endIntegerString = parsedResponse.end.match(
509
+ /\d+/
510
+ )?.[0];
511
+ const multipliers = {
512
+ second: 1 * 1e3,
513
+ minute: 60 * 1e3,
514
+ hour: 3600 * 1e3,
515
+ day: 86400 * 1e3
516
+ };
517
+ const startMultiplier = parsedResponse.start.match(
518
+ /second|minute|hour|day/
519
+ )?.[0];
520
+ const endMultiplier = parsedResponse.end.match(
521
+ /second|minute|hour|day/
522
+ )?.[0];
523
+ const startInteger = startIntegerString ? Number.parseInt(startIntegerString) : 0;
524
+ const endInteger = endIntegerString ? Number.parseInt(endIntegerString) : 0;
525
+ const startTime = startInteger * multipliers[startMultiplier];
526
+ console.log("startTime", startTime);
527
+ const endTime = endInteger * multipliers[endMultiplier];
528
+ console.log("endTime", endTime);
529
+ parsedResponse.start = Date.now() - startTime;
530
+ parsedResponse.end = Date.now() - endTime;
531
+ return parsedResponse;
532
+ }
533
+ }
534
+ }
535
+ };
536
+ var summarizeAction2 = {
537
+ name: "SUMMARIZE_CONVERSATION",
538
+ similes: [
539
+ "RECAP",
540
+ "RECAP_CONVERSATION",
541
+ "SUMMARIZE_CHAT",
542
+ "SUMMARIZATION",
543
+ "CHAT_SUMMARY",
544
+ "CONVERSATION_SUMMARY"
545
+ ],
546
+ description: "Summarizes the conversation and attachments.",
547
+ validate: async (_runtime, message, _state) => {
548
+ if (message.content.source !== "discord") {
549
+ return false;
550
+ }
551
+ const keywords = [
552
+ "summarize",
553
+ "summarization",
554
+ "summary",
555
+ "recap",
556
+ "report",
557
+ "overview",
558
+ "review",
559
+ "rundown",
560
+ "wrap-up",
561
+ "brief",
562
+ "debrief",
563
+ "abstract",
564
+ "synopsis",
565
+ "outline",
566
+ "digest",
567
+ "abridgment",
568
+ "condensation",
569
+ "encapsulation",
570
+ "essence",
571
+ "gist",
572
+ "main points",
573
+ "key points",
574
+ "key takeaways",
575
+ "bulletpoint",
576
+ "highlights",
577
+ "tldr",
578
+ "tl;dr",
579
+ "in a nutshell",
580
+ "bottom line",
581
+ "long story short",
582
+ "sum up",
583
+ "sum it up",
584
+ "short version",
585
+ "bring me up to speed",
586
+ "catch me up"
587
+ ];
588
+ return keywords.some(
589
+ (keyword) => message.content.text.toLowerCase().includes(keyword.toLowerCase())
590
+ );
591
+ },
592
+ handler: async (runtime, message, state, _options, callback) => {
593
+ const callbackData = {
594
+ text: "",
595
+ // fill in later
596
+ actions: ["SUMMARIZATION_RESPONSE"],
597
+ source: message.content.source,
598
+ attachments: []
599
+ };
600
+ const { roomId } = message;
601
+ const dateRange = await getDateRange(runtime, message, state);
602
+ if (!dateRange) {
603
+ console.error("Couldn't get date range from message");
604
+ await runtime.getMemoryManager("messages").createMemory({
605
+ entityId: message.entityId,
606
+ agentId: message.agentId,
607
+ roomId: message.roomId,
608
+ content: {
609
+ source: "discord",
610
+ thought: `I couldn't get the date range from the message`,
611
+ actions: ["SUMMARIZE_CONVERSATION_FAILED"]
612
+ },
613
+ metadata: {
614
+ type: "SUMMARIZE_CONVERSATION"
615
+ }
616
+ });
617
+ return;
618
+ }
619
+ const { objective, start, end } = dateRange;
620
+ const memories = await runtime.getMemoryManager("messages").getMemories({
621
+ roomId,
622
+ // subtract start from current time
623
+ start: Number.parseInt(start),
624
+ end: Number.parseInt(end),
625
+ count: 1e4,
626
+ unique: false
627
+ });
628
+ const entities = await getEntityDetails({
629
+ runtime,
630
+ roomId
631
+ });
632
+ const actorMap = new Map(entities.map((entity) => [entity.id, entity]));
633
+ const formattedMemories = memories.map((memory) => {
634
+ const attachments = memory.content.attachments?.map((attachment) => {
635
+ return `---
636
+ Attachment: ${attachment.id}
637
+ ${attachment.description}
638
+ ${attachment.text}
639
+ ---`;
640
+ }).join("\n");
641
+ return `${actorMap.get(memory.entityId)?.name ?? "Unknown User"} (${actorMap.get(memory.entityId)?.username ?? ""}): ${memory.content.text}
642
+ ${attachments}`;
643
+ }).join("\n");
644
+ let currentSummary = "";
645
+ const chunkSize = 8e3;
646
+ const chunks = await splitChunks(formattedMemories, chunkSize, 0);
647
+ const _datestr = (/* @__PURE__ */ new Date()).toUTCString().replace(/:/g, "-");
648
+ state.values.memoriesWithAttachments = formattedMemories;
649
+ state.values.objective = objective;
650
+ for (let i = 0; i < chunks.length; i++) {
651
+ const chunk = chunks[i];
652
+ state.values.currentSummary = currentSummary;
653
+ state.values.currentChunk = chunk;
654
+ const template = await trimTokens2(
655
+ summarizationTemplate2,
656
+ chunkSize + 500,
657
+ runtime
658
+ );
659
+ const prompt = composePrompt3({
660
+ state,
661
+ // make sure it fits, we can pad the tokens a bit
662
+ template
663
+ });
664
+ const summary = await runtime.useModel(ModelTypes3.TEXT_SMALL, {
665
+ prompt
666
+ });
667
+ currentSummary = `${currentSummary}
668
+ ${summary}`;
669
+ }
670
+ if (!currentSummary) {
671
+ console.error("No summary found, that's not good!");
672
+ await runtime.getMemoryManager("messages").createMemory({
673
+ entityId: message.entityId,
674
+ agentId: message.agentId,
675
+ roomId: message.roomId,
676
+ content: {
677
+ source: "discord",
678
+ thought: `I couldn't summarize the conversation`,
679
+ actions: ["SUMMARIZE_CONVERSATION_FAILED"]
680
+ },
681
+ metadata: {
682
+ type: "SUMMARIZE_CONVERSATION"
683
+ }
684
+ });
685
+ return;
686
+ }
687
+ callbackData.text = currentSummary.trim();
688
+ if (callbackData.text && (currentSummary.trim()?.split("\n").length < 4 || currentSummary.trim()?.split(" ").length < 100)) {
689
+ callbackData.text = `Here is the summary:
690
+ \`\`\`md
691
+ ${currentSummary.trim()}
692
+ \`\`\`
693
+ `;
694
+ await callback(callbackData);
695
+ } else if (currentSummary.trim()) {
696
+ const summaryDir = "cache";
697
+ const summaryFilename = `${summaryDir}/conversation_summary_${Date.now()}`;
698
+ await runtime.getDatabaseAdapter().setCache(summaryFilename, currentSummary);
699
+ await fs2.promises.mkdir(summaryDir, { recursive: true });
700
+ await fs2.promises.writeFile(summaryFilename, currentSummary, "utf8");
701
+ await callback(
702
+ {
703
+ ...callbackData,
704
+ text: `I've attached the summary of the conversation from \`${new Date(Number.parseInt(start)).toString()}\` to \`${new Date(Number.parseInt(end)).toString()}\` as a text file.`
705
+ },
706
+ [summaryFilename]
707
+ );
708
+ } else {
709
+ console.warn(
710
+ "Empty response from summarize conversation action, skipping"
711
+ );
712
+ }
713
+ return callbackData;
714
+ },
715
+ examples: [
716
+ [
717
+ {
718
+ name: "{{name1}}",
719
+ content: {
720
+ text: "```js\nconst x = 10\n```"
721
+ }
722
+ },
723
+ {
724
+ name: "{{name1}}",
725
+ content: {
726
+ text: "can you give me a detailed report on what we're talking about?"
727
+ }
728
+ },
729
+ {
730
+ name: "{{name2}}",
731
+ content: {
732
+ text: "sure, no problem, give me a minute to get that together for you",
733
+ actions: ["SUMMARIZE"]
734
+ }
735
+ }
736
+ ],
737
+ [
738
+ {
739
+ name: "{{name1}}",
740
+ content: {
741
+ text: "please summarize the conversation we just had and include this blogpost i'm linking (Attachment: b3e12)"
742
+ }
743
+ },
744
+ {
745
+ name: "{{name2}}",
746
+ content: {
747
+ text: "sure, give me a sec",
748
+ actions: ["SUMMARIZE"]
749
+ }
750
+ }
751
+ ],
752
+ [
753
+ {
754
+ name: "{{name1}}",
755
+ content: {
756
+ text: "Can you summarize what moon and avf are talking about?"
757
+ }
758
+ },
759
+ {
760
+ name: "{{name2}}",
761
+ content: {
762
+ text: "Yeah, just hold on a second while I get that together for you...",
763
+ actions: ["SUMMARIZE"]
764
+ }
765
+ }
766
+ ],
767
+ [
768
+ {
769
+ name: "{{name1}}",
770
+ content: {
771
+ text: "i need to write a blog post about farming, can you summarize the discussion from a few hours ago?"
772
+ }
773
+ },
774
+ {
775
+ name: "{{name2}}",
776
+ content: {
777
+ text: "no problem, give me a few minutes to read through everything",
778
+ actions: ["SUMMARIZE"]
779
+ }
780
+ }
781
+ ]
782
+ ]
783
+ };
784
+ var summarizeConversation_default = summarizeAction2;
785
+
786
+ // src/actions/transcribeMedia.ts
787
+ import {
788
+ composePrompt as composePrompt4,
789
+ ModelTypes as ModelTypes4,
790
+ parseJSONObjectFromText as parseJSONObjectFromText4
791
+ } from "@elizaos/core";
792
+ var mediaAttachmentIdTemplate = `# Messages we are transcribing
793
+ {{recentMessages}}
794
+
795
+ # Instructions: {{senderName}} is requesting a transcription of a specific media file (audio or video). Your goal is to determine the ID of the attachment they want transcribed.
796
+ The "attachmentId" is the ID of the media file attachment that the user wants transcribed. If not specified, return null.
797
+
798
+ Your response must be formatted as a JSON block with this structure:
799
+ \`\`\`json
800
+ {
801
+ "attachmentId": "<Attachment ID>"
802
+ }
803
+ \`\`\`
804
+ `;
805
+ var getMediaAttachmentId = async (runtime, _message, state) => {
806
+ const prompt = composePrompt4({
807
+ state,
808
+ template: mediaAttachmentIdTemplate
809
+ });
810
+ for (let i = 0; i < 5; i++) {
811
+ const response = await runtime.useModel(ModelTypes4.TEXT_SMALL, {
812
+ prompt
813
+ });
814
+ console.log("response", response);
815
+ const parsedResponse = parseJSONObjectFromText4(response);
816
+ if (parsedResponse?.attachmentId) {
817
+ return parsedResponse.attachmentId;
818
+ }
819
+ }
820
+ return null;
821
+ };
822
+ var transcribeMediaAction = {
823
+ name: "TRANSCRIBE_MEDIA",
824
+ similes: [
825
+ "TRANSCRIBE_AUDIO",
826
+ "TRANSCRIBE_VIDEO",
827
+ "MEDIA_TRANSCRIPT",
828
+ "VIDEO_TRANSCRIPT",
829
+ "AUDIO_TRANSCRIPT"
830
+ ],
831
+ description: "Transcribe the full text of an audio or video file that the user has attached.",
832
+ validate: async (_runtime, message, _state) => {
833
+ if (message.content.source !== "discord") {
834
+ return false;
835
+ }
836
+ const keywords = [
837
+ "transcribe",
838
+ "transcript",
839
+ "audio",
840
+ "video",
841
+ "media",
842
+ "youtube",
843
+ "meeting",
844
+ "recording",
845
+ "podcast",
846
+ "call",
847
+ "conference",
848
+ "interview",
849
+ "speech",
850
+ "lecture",
851
+ "presentation"
852
+ ];
853
+ return keywords.some(
854
+ (keyword) => message.content.text.toLowerCase().includes(keyword.toLowerCase())
855
+ );
856
+ },
857
+ handler: async (runtime, message, state, _options, callback) => {
858
+ const callbackData = {
859
+ text: "",
860
+ // fill in later
861
+ actions: ["TRANSCRIBE_MEDIA_RESPONSE"],
862
+ source: message.content.source,
863
+ attachments: []
864
+ };
865
+ const attachmentId = await getMediaAttachmentId(runtime, message, state);
866
+ if (!attachmentId) {
867
+ console.error("Couldn't get media attachment ID from message");
868
+ await runtime.getMemoryManager("messages").createMemory({
869
+ entityId: message.entityId,
870
+ agentId: message.agentId,
871
+ roomId: message.roomId,
872
+ content: {
873
+ source: "discord",
874
+ thought: `I couldn't find the media attachment ID in the message`,
875
+ actions: ["TRANSCRIBE_MEDIA_FAILED"]
876
+ },
877
+ metadata: {
878
+ type: "TRANSCRIBE_MEDIA"
879
+ }
880
+ });
881
+ return;
882
+ }
883
+ const attachment = state.data.recentMessages.filter(
884
+ (msg) => msg.content.attachments && msg.content.attachments.length > 0
885
+ ).flatMap((msg) => msg.content.attachments).find(
886
+ (attachment2) => attachment2.id.toLowerCase() === attachmentId.toLowerCase()
887
+ );
888
+ if (!attachment) {
889
+ console.error(`Couldn't find attachment with ID ${attachmentId}`);
890
+ await runtime.getMemoryManager("messages").createMemory({
891
+ entityId: message.entityId,
892
+ agentId: message.agentId,
893
+ roomId: message.roomId,
894
+ content: {
895
+ source: "discord",
896
+ thought: `I couldn't find the media attachment with ID ${attachmentId}`,
897
+ actions: ["TRANSCRIBE_MEDIA_FAILED"]
898
+ },
899
+ metadata: {
900
+ type: "TRANSCRIBE_MEDIA"
901
+ }
902
+ });
903
+ return;
904
+ }
905
+ const mediaTranscript = attachment.text;
906
+ callbackData.text = mediaTranscript.trim();
907
+ if (callbackData.text && (callbackData.text?.split("\n").length < 4 || callbackData.text?.split(" ").length < 100)) {
908
+ callbackData.text = `Here is the transcript:
909
+ \`\`\`md
910
+ ${mediaTranscript.trim()}
911
+ \`\`\`
912
+ `;
913
+ await callback(callbackData);
914
+ } else if (callbackData.text) {
915
+ const transcriptFilename = `content/transcript_${Date.now()}`;
916
+ await runtime.getDatabaseAdapter().setCache(transcriptFilename, callbackData.text);
917
+ await callback(
918
+ {
919
+ ...callbackData,
920
+ text: `I've attached the transcript as a text file.`
921
+ },
922
+ [transcriptFilename]
923
+ );
924
+ } else {
925
+ console.warn("Empty response from transcribe media action, skipping");
926
+ }
927
+ return callbackData;
928
+ },
929
+ examples: [
930
+ [
931
+ {
932
+ name: "{{name1}}",
933
+ content: {
934
+ text: "Please transcribe the audio file I just sent."
935
+ }
936
+ },
937
+ {
938
+ name: "{{name2}}",
939
+ content: {
940
+ text: "Sure, I'll transcribe the full audio for you.",
941
+ actions: ["TRANSCRIBE_MEDIA"]
942
+ }
943
+ }
944
+ ],
945
+ [
946
+ {
947
+ name: "{{name1}}",
948
+ content: {
949
+ text: "Can I get a transcript of that video recording?"
950
+ }
951
+ },
952
+ {
953
+ name: "{{name2}}",
954
+ content: {
955
+ text: "Absolutely, give me a moment to generate the full transcript of the video.",
956
+ actions: ["TRANSCRIBE_MEDIA"]
957
+ }
958
+ }
959
+ ]
960
+ ]
961
+ };
962
+ var transcribeMedia_default = transcribeMediaAction;
963
+
964
+ // src/actions/voiceJoin.ts
965
+ import {
966
+ ChannelType as ChannelType2,
967
+ composePrompt as composePrompt5,
968
+ createUniqueUuid as createUniqueUuid2,
969
+ logger,
970
+ ModelTypes as ModelTypes5
971
+ } from "@elizaos/core";
972
+ import {
973
+ ChannelType as DiscordChannelType
974
+ } from "discord.js";
975
+
976
+ // src/types.ts
977
+ var ServiceTypes2 = {
978
+ DISCORD: "discord"
979
+ };
980
+
981
+ // src/actions/voiceJoin.ts
982
+ var voiceJoin_default = {
983
+ name: "JOIN_VOICE",
984
+ similes: [
985
+ "JOIN_VOICE",
986
+ "JOIN_VC",
987
+ "JOIN_VOICE_CHAT",
988
+ "JOIN_VOICE_CHANNEL",
989
+ "JOIN_MEETING",
990
+ "JOIN_CALL"
991
+ ],
992
+ validate: async (runtime, message, state) => {
993
+ if (message.content.source !== "discord") {
994
+ return false;
995
+ }
996
+ const room = state.data.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
997
+ if (room?.type !== ChannelType2.GROUP) {
998
+ return false;
999
+ }
1000
+ const client = runtime.getService(ServiceTypes2.DISCORD);
1001
+ if (!client) {
1002
+ logger.error("Discord client not found");
1003
+ return false;
1004
+ }
1005
+ return true;
1006
+ },
1007
+ description: "Join a voice channel to participate in voice chat.",
1008
+ handler: async (runtime, message, state, _options, callback) => {
1009
+ const room = state.data.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
1010
+ if (!room) {
1011
+ throw new Error("No room found");
1012
+ }
1013
+ if (room.type !== ChannelType2.GROUP) {
1014
+ return false;
1015
+ }
1016
+ console.log("Running handler on provider", room.name);
1017
+ const serverId = room.serverId;
1018
+ if (!serverId) {
1019
+ throw new Error("No server ID found");
1020
+ }
1021
+ const discordClient = runtime.getService(
1022
+ ServiceTypes2.DISCORD
1023
+ );
1024
+ const client = discordClient.client;
1025
+ const voiceManager = discordClient.voiceManager;
1026
+ if (!client) {
1027
+ logger.error("Discord client not found");
1028
+ return false;
1029
+ }
1030
+ const voiceChannels = client.guilds.cache.get(serverId).channels.cache.filter(
1031
+ (channel) => channel.type === DiscordChannelType.GuildVoice
1032
+ );
1033
+ const targetChannel = voiceChannels.find((channel) => {
1034
+ const name = channel.name.toLowerCase();
1035
+ const messageContent = message?.content?.text;
1036
+ const replacedName = name.replace(/[^a-z0-9 ]/g, "");
1037
+ return name.includes(messageContent) || messageContent.includes(name) || replacedName.includes(messageContent) || messageContent.includes(replacedName);
1038
+ });
1039
+ if (targetChannel) {
1040
+ voiceManager.joinChannel(targetChannel);
1041
+ return true;
1042
+ }
1043
+ const guild = client.guilds.cache.get(serverId);
1044
+ const members = guild?.members.cache;
1045
+ const member = members?.find(
1046
+ (member2) => createUniqueUuid2(runtime, member2.id) === message.entityId
1047
+ );
1048
+ if (member?.voice?.channel) {
1049
+ voiceManager.joinChannel(member?.voice?.channel);
1050
+ await runtime.getMemoryManager("messages").createMemory({
1051
+ entityId: message.entityId,
1052
+ agentId: message.agentId,
1053
+ roomId: message.roomId,
1054
+ content: {
1055
+ source: "discord",
1056
+ thought: `I joined the voice channel ${member?.voice?.channel?.name}`,
1057
+ actions: ["JOIN_VOICE_STARTED"]
1058
+ },
1059
+ metadata: {
1060
+ type: "JOIN_VOICE"
1061
+ }
1062
+ });
1063
+ await runtime.getMemoryManager("messages").createMemory({
1064
+ entityId: message.entityId,
1065
+ agentId: message.agentId,
1066
+ roomId: createUniqueUuid2(runtime, targetChannel.id),
1067
+ content: {
1068
+ source: "discord",
1069
+ thought: `I joined the voice channel ${targetChannel.name}`,
1070
+ actions: ["JOIN_VOICE_STARTED"]
1071
+ },
1072
+ metadata: {
1073
+ type: "JOIN_VOICE"
1074
+ }
1075
+ });
1076
+ return true;
1077
+ }
1078
+ const messageTemplate = `
1079
+ The user has requested to join a voice channel.
1080
+ Here is the list of channels available in the server:
1081
+ {{voiceChannels}}
1082
+
1083
+ Here is the user's request:
1084
+ {{userMessage}}
1085
+
1086
+ Please respond with the name of the voice channel which the bot should join. Try to infer what channel the user is talking about. If the user didn't specify a voice channel, respond with "none".
1087
+ You should only respond with the name of the voice channel or none, no commentary or additional information should be included.
1088
+ `;
1089
+ const guessState = {
1090
+ userMessage: message.content.text,
1091
+ voiceChannels: voiceChannels.map((channel) => channel.name).join("\n")
1092
+ };
1093
+ const prompt = composePrompt5({
1094
+ template: messageTemplate,
1095
+ state: guessState
1096
+ });
1097
+ const responseContent = await runtime.useModel(ModelTypes5.TEXT_SMALL, {
1098
+ prompt
1099
+ });
1100
+ if (responseContent && responseContent.trim().length > 0) {
1101
+ const channelName = responseContent.toLowerCase();
1102
+ const targetChannel2 = voiceChannels.find((channel) => {
1103
+ const name = channel.name.toLowerCase();
1104
+ const replacedName = name.replace(/[^a-z0-9 ]/g, "");
1105
+ return name.includes(channelName) || channelName.includes(name) || replacedName.includes(channelName) || channelName.includes(replacedName);
1106
+ });
1107
+ if (targetChannel2) {
1108
+ voiceManager.joinChannel(targetChannel2);
1109
+ await runtime.getMemoryManager("messages").createMemory({
1110
+ entityId: message.entityId,
1111
+ agentId: message.agentId,
1112
+ roomId: message.roomId,
1113
+ content: {
1114
+ source: "discord",
1115
+ thought: `I joined the voice channel ${member?.voice?.channel?.name}`,
1116
+ actions: ["JOIN_VOICE_STARTED"]
1117
+ },
1118
+ metadata: {
1119
+ type: "JOIN_VOICE"
1120
+ }
1121
+ });
1122
+ await runtime.getMemoryManager("messages").createMemory({
1123
+ entityId: message.entityId,
1124
+ agentId: message.agentId,
1125
+ roomId: createUniqueUuid2(runtime, targetChannel2.id),
1126
+ content: {
1127
+ source: "discord",
1128
+ thought: `I joined the voice channel ${targetChannel2.name}`,
1129
+ actions: ["JOIN_VOICE_STARTED"]
1130
+ },
1131
+ metadata: {
1132
+ type: "JOIN_VOICE"
1133
+ }
1134
+ });
1135
+ return true;
1136
+ }
1137
+ }
1138
+ await callback({
1139
+ text: "I couldn't figure out which channel you wanted me to join.",
1140
+ source: "discord"
1141
+ });
1142
+ return false;
1143
+ },
1144
+ examples: [
1145
+ [
1146
+ {
1147
+ name: "{{name1}}",
1148
+ content: {
1149
+ text: "Hey, let's jump into the 'General' voice and chat"
1150
+ }
1151
+ },
1152
+ {
1153
+ name: "{{name2}}",
1154
+ content: {
1155
+ text: "Sounds good",
1156
+ actions: ["JOIN_VOICE"]
1157
+ }
1158
+ }
1159
+ ],
1160
+ [
1161
+ {
1162
+ name: "{{name1}}",
1163
+ content: {
1164
+ text: "{{name2}}, can you join the vc, I want to discuss our strat"
1165
+ }
1166
+ },
1167
+ {
1168
+ name: "{{name2}}",
1169
+ content: {
1170
+ text: "Sure I'll join right now",
1171
+ actions: ["JOIN_VOICE"]
1172
+ }
1173
+ }
1174
+ ],
1175
+ [
1176
+ {
1177
+ name: "{{name1}}",
1178
+ content: {
1179
+ text: "hey {{name2}}, we're having a team meeting in the 'conference' voice channel, plz join us"
1180
+ }
1181
+ },
1182
+ {
1183
+ name: "{{name2}}",
1184
+ content: {
1185
+ text: "OK see you there",
1186
+ actions: ["JOIN_VOICE"]
1187
+ }
1188
+ }
1189
+ ],
1190
+ [
1191
+ {
1192
+ name: "{{name1}}",
1193
+ content: {
1194
+ text: "{{name2}}, let's have a quick voice chat in the 'Lounge' channel."
1195
+ }
1196
+ },
1197
+ {
1198
+ name: "{{name2}}",
1199
+ content: {
1200
+ text: "kk be there in a sec",
1201
+ actions: ["JOIN_VOICE"]
1202
+ }
1203
+ }
1204
+ ],
1205
+ [
1206
+ {
1207
+ name: "{{name1}}",
1208
+ content: {
1209
+ text: "Hey {{name2}}, can you join me in the 'Music' voice channel"
1210
+ }
1211
+ },
1212
+ {
1213
+ name: "{{name2}}",
1214
+ content: {
1215
+ text: "Sure",
1216
+ actions: ["JOIN_VOICE"]
1217
+ }
1218
+ }
1219
+ ],
1220
+ [
1221
+ {
1222
+ name: "{{name1}}",
1223
+ content: {
1224
+ text: "join voice chat with us {{name2}}"
1225
+ }
1226
+ },
1227
+ {
1228
+ name: "{{name2}}",
1229
+ content: {
1230
+ text: "coming",
1231
+ actions: ["JOIN_VOICE"]
1232
+ }
1233
+ }
1234
+ ],
1235
+ [
1236
+ {
1237
+ name: "{{name1}}",
1238
+ content: {
1239
+ text: "hop in vc {{name2}}"
1240
+ }
1241
+ },
1242
+ {
1243
+ name: "{{name2}}",
1244
+ content: {
1245
+ text: "joining now",
1246
+ actions: ["JOIN_VOICE"]
1247
+ }
1248
+ }
1249
+ ],
1250
+ [
1251
+ {
1252
+ name: "{{name1}}",
1253
+ content: {
1254
+ text: "get in vc with us {{name2}}"
1255
+ }
1256
+ },
1257
+ {
1258
+ name: "{{name2}}",
1259
+ content: {
1260
+ text: "im in",
1261
+ actions: ["JOIN_VOICE"]
1262
+ }
1263
+ }
1264
+ ]
1265
+ ]
1266
+ };
1267
+
1268
+ // src/actions/voiceLeave.ts
1269
+ import {
1270
+ ChannelType as ChannelType3,
1271
+ createUniqueUuid as createUniqueUuid3,
1272
+ logger as logger2
1273
+ } from "@elizaos/core";
1274
+ import { BaseGuildVoiceChannel } from "discord.js";
1275
+ var voiceLeave_default = {
1276
+ name: "LEAVE_VOICE",
1277
+ similes: [
1278
+ "LEAVE_VOICE",
1279
+ "LEAVE_VC",
1280
+ "LEAVE_VOICE_CHAT",
1281
+ "LEAVE_VOICE_CHANNEL",
1282
+ "LEAVE_MEETING",
1283
+ "LEAVE_CALL"
1284
+ ],
1285
+ validate: async (runtime, message, state) => {
1286
+ if (message.content.source !== "discord") {
1287
+ return false;
1288
+ }
1289
+ const service = runtime.getService(ServiceTypes2.DISCORD);
1290
+ if (!service) {
1291
+ logger2.error("Discord client not found");
1292
+ return false;
1293
+ }
1294
+ const room = state.data.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
1295
+ if (room?.type !== ChannelType3.GROUP) {
1296
+ return false;
1297
+ }
1298
+ const isConnectedToVoice = service.client.voice.adapters.size > 0;
1299
+ return isConnectedToVoice;
1300
+ },
1301
+ description: "Leave the current voice channel.",
1302
+ handler: async (runtime, message, _state, _options) => {
1303
+ const room = await runtime.getDatabaseAdapter().getRoom(message.roomId);
1304
+ if (!room) {
1305
+ throw new Error("No room found");
1306
+ }
1307
+ if (room.type !== ChannelType3.GROUP) {
1308
+ throw new Error("Not a group");
1309
+ }
1310
+ const serverId = room.serverId;
1311
+ if (!serverId) {
1312
+ throw new Error("No server ID found 9");
1313
+ }
1314
+ const discordClient = runtime.getService(
1315
+ ServiceTypes2.DISCORD
1316
+ );
1317
+ const voiceManager = discordClient.voiceManager;
1318
+ const client = discordClient.client;
1319
+ if (!client) {
1320
+ logger2.error("Discord client not found");
1321
+ throw new Error("Discord client not found");
1322
+ }
1323
+ if (!voiceManager) {
1324
+ logger2.error("voiceManager is not available.");
1325
+ throw new Error("voiceManager is not available.");
1326
+ }
1327
+ const guild = client.guilds.cache.get(serverId);
1328
+ if (!guild) {
1329
+ console.warn("Bot is not in any voice channel.");
1330
+ await runtime.getMemoryManager("messages").createMemory({
1331
+ entityId: message.entityId,
1332
+ agentId: message.agentId,
1333
+ roomId: message.roomId,
1334
+ content: {
1335
+ source: "discord",
1336
+ thought: "I tried to leave the voice channel but I'm not in any voice channel.",
1337
+ actions: ["LEAVE_VOICE"]
1338
+ },
1339
+ metadata: {
1340
+ type: "LEAVE_VOICE"
1341
+ }
1342
+ });
1343
+ return false;
1344
+ }
1345
+ const voiceChannel = guild.members.me?.voice.channel;
1346
+ if (!voiceChannel || !(voiceChannel instanceof BaseGuildVoiceChannel)) {
1347
+ console.warn("Could not retrieve the voice channel.");
1348
+ await runtime.getMemoryManager("messages").createMemory({
1349
+ entityId: message.entityId,
1350
+ agentId: message.agentId,
1351
+ roomId: message.roomId,
1352
+ content: {
1353
+ source: "discord",
1354
+ thought: "I tried to leave the voice channel but I couldn't find it.",
1355
+ actions: ["LEAVE_VOICE"]
1356
+ },
1357
+ metadata: {
1358
+ type: "LEAVE_VOICE"
1359
+ }
1360
+ });
1361
+ return false;
1362
+ }
1363
+ const connection = voiceManager.getVoiceConnection(guild.id);
1364
+ if (!connection) {
1365
+ console.warn("No active voice connection found for the bot.");
1366
+ await runtime.getMemoryManager("messages").createMemory({
1367
+ entityId: message.entityId,
1368
+ agentId: message.agentId,
1369
+ roomId: message.roomId,
1370
+ content: {
1371
+ source: "discord",
1372
+ thought: "I tried to leave the voice channel but I couldn't find the connection.",
1373
+ actions: ["LEAVE_VOICE"]
1374
+ },
1375
+ metadata: {
1376
+ type: "LEAVE_VOICE"
1377
+ }
1378
+ });
1379
+ return false;
1380
+ }
1381
+ voiceManager.leaveChannel(voiceChannel);
1382
+ await runtime.getMemoryManager("messages").createMemory({
1383
+ entityId: message.entityId,
1384
+ agentId: message.agentId,
1385
+ roomId: createUniqueUuid3(runtime, voiceChannel.id),
1386
+ content: {
1387
+ source: "discord",
1388
+ thought: `I left the voice channel ${voiceChannel.name}`,
1389
+ actions: ["LEAVE_VOICE_STARTED"]
1390
+ },
1391
+ metadata: {
1392
+ type: "LEAVE_VOICE"
1393
+ }
1394
+ });
1395
+ return true;
1396
+ },
1397
+ examples: [
1398
+ [
1399
+ {
1400
+ name: "{{name1}}",
1401
+ content: {
1402
+ text: "Hey {{name2}} please leave the voice channel"
1403
+ }
1404
+ },
1405
+ {
1406
+ name: "{{name2}}",
1407
+ content: {
1408
+ text: "Sure",
1409
+ actions: ["LEAVE_VOICE"]
1410
+ }
1411
+ }
1412
+ ],
1413
+ [
1414
+ {
1415
+ name: "{{name1}}",
1416
+ content: {
1417
+ text: "I have to go now but thanks for the chat"
1418
+ }
1419
+ },
1420
+ {
1421
+ name: "{{name2}}",
1422
+ content: {
1423
+ text: "You too, talk to you later",
1424
+ actions: ["LEAVE_VOICE"]
1425
+ }
1426
+ }
1427
+ ],
1428
+ [
1429
+ {
1430
+ name: "{{name1}}",
1431
+ content: {
1432
+ text: "Great call everyone, hopping off now",
1433
+ actions: ["LEAVE_VOICE"]
1434
+ }
1435
+ },
1436
+ {
1437
+ name: "{{name2}}",
1438
+ content: {
1439
+ text: "Agreed, I'll hop off too",
1440
+ actions: ["LEAVE_VOICE"]
1441
+ }
1442
+ }
1443
+ ],
1444
+ [
1445
+ {
1446
+ name: "{{name1}}",
1447
+ content: {
1448
+ text: "Hey {{name2}} I need you to step away from the voice chat for a bit"
1449
+ }
1450
+ },
1451
+ {
1452
+ name: "{{name2}}",
1453
+ content: {
1454
+ text: "No worries, I'll leave the voice channel",
1455
+ actions: ["LEAVE_VOICE"]
1456
+ }
1457
+ }
1458
+ ],
1459
+ [
1460
+ {
1461
+ name: "{{name1}}",
1462
+ content: {
1463
+ text: "{{name2}}, I think we covered everything, you can leave the voice chat now"
1464
+ }
1465
+ },
1466
+ {
1467
+ name: "{{name2}}",
1468
+ content: {
1469
+ text: "Sounds good, see you both later",
1470
+ actions: ["LEAVE_VOICE"]
1471
+ }
1472
+ }
1473
+ ],
1474
+ [
1475
+ {
1476
+ name: "{{name1}}",
1477
+ content: {
1478
+ text: "leave voice {{name2}}"
1479
+ }
1480
+ },
1481
+ {
1482
+ name: "{{name2}}",
1483
+ content: {
1484
+ text: "ok leaving",
1485
+ actions: ["LEAVE_VOICE"]
1486
+ }
1487
+ }
1488
+ ],
1489
+ [
1490
+ {
1491
+ name: "{{name1}}",
1492
+ content: {
1493
+ text: "plz leave the voice chat {{name2}}"
1494
+ }
1495
+ },
1496
+ {
1497
+ name: "{{name2}}",
1498
+ content: {
1499
+ text: "aight im out",
1500
+ actions: ["LEAVE_VOICE"]
1501
+ }
1502
+ }
1503
+ ],
1504
+ [
1505
+ {
1506
+ name: "{{name1}}",
1507
+ content: {
1508
+ text: "yo {{name2}} gtfo the vc"
1509
+ }
1510
+ },
1511
+ {
1512
+ name: "{{name2}}",
1513
+ content: {
1514
+ text: "sorry, talk to you later",
1515
+ actions: ["LEAVE_VOICE"]
1516
+ }
1517
+ }
1518
+ ]
1519
+ ]
1520
+ };
1521
+
1522
+ // src/constants.ts
1523
+ var MESSAGE_CONSTANTS = {
1524
+ MAX_MESSAGES: 10,
1525
+ RECENT_MESSAGE_COUNT: 3,
1526
+ CHAT_HISTORY_COUNT: 5,
1527
+ INTEREST_DECAY_TIME: 5 * 60 * 1e3,
1528
+ // 5 minutes
1529
+ PARTIAL_INTEREST_DECAY: 3 * 60 * 1e3,
1530
+ // 3 minutes
1531
+ DEFAULT_SIMILARITY_THRESHOLD: 0.3,
1532
+ DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.2
1533
+ };
1534
+ var DISCORD_SERVICE_NAME = "discord";
1535
+
1536
+ // src/messages.ts
1537
+ import {
1538
+ ChannelType as ChannelType5,
1539
+ createUniqueUuid as createUniqueUuid4,
1540
+ logger as logger4,
1541
+ ServiceTypes as ServiceTypes4
1542
+ } from "@elizaos/core";
1543
+ import {
1544
+ ChannelType as DiscordChannelType2
1545
+ } from "discord.js";
1546
+
1547
+ // src/attachments.ts
1548
+ import { trimTokens as trimTokens3 } from "@elizaos/core";
1549
+ import { parseJSONObjectFromText as parseJSONObjectFromText5 } from "@elizaos/core";
1550
+ import {
1551
+ ModelTypes as ModelTypes6,
1552
+ ServiceTypes as ServiceTypes3
1553
+ } from "@elizaos/core";
1554
+ import { Collection } from "discord.js";
1555
+ import ffmpeg from "fluent-ffmpeg";
1556
+ import fs3 from "node:fs";
1557
+ async function generateSummary(runtime, text) {
1558
+ text = await trimTokens3(text, 1e5, runtime);
1559
+ const prompt = `Please generate a concise summary for the following text:
1560
+
1561
+ Text: """
1562
+ ${text}
1563
+ """
1564
+
1565
+ Respond with a JSON object in the following format:
1566
+ \`\`\`json
1567
+ {
1568
+ "title": "Generated Title",
1569
+ "summary": "Generated summary and/or description of the text"
1570
+ }
1571
+ \`\`\``;
1572
+ const response = await runtime.useModel(ModelTypes6.TEXT_SMALL, {
1573
+ prompt
1574
+ });
1575
+ const parsedResponse = parseJSONObjectFromText5(response);
1576
+ if (parsedResponse?.title && parsedResponse?.summary) {
1577
+ return {
1578
+ title: parsedResponse.title,
1579
+ description: parsedResponse.summary
1580
+ };
1581
+ }
1582
+ return {
1583
+ title: "",
1584
+ description: ""
1585
+ };
1586
+ }
1587
+ var AttachmentManager = class {
1588
+ attachmentCache = /* @__PURE__ */ new Map();
1589
+ runtime;
1590
+ constructor(runtime) {
1591
+ this.runtime = runtime;
1592
+ }
1593
+ async processAttachments(attachments) {
1594
+ const processedAttachments = [];
1595
+ const attachmentCollection = attachments instanceof Collection ? attachments : new Collection(attachments.map((att) => [att.id, att]));
1596
+ for (const [, attachment] of attachmentCollection) {
1597
+ const media = await this.processAttachment(attachment);
1598
+ if (media) {
1599
+ processedAttachments.push(media);
1600
+ }
1601
+ }
1602
+ return processedAttachments;
1603
+ }
1604
+ async processAttachment(attachment) {
1605
+ if (this.attachmentCache.has(attachment.url)) {
1606
+ return this.attachmentCache.get(attachment.url);
1607
+ }
1608
+ let media = null;
1609
+ if (attachment.contentType?.startsWith("application/pdf")) {
1610
+ media = await this.processPdfAttachment(attachment);
1611
+ } else if (attachment.contentType?.startsWith("text/plain")) {
1612
+ media = await this.processPlaintextAttachment(attachment);
1613
+ } else if (attachment.contentType?.startsWith("audio/") || attachment.contentType?.startsWith("video/mp4")) {
1614
+ media = await this.processAudioVideoAttachment(attachment);
1615
+ } else if (attachment.contentType?.startsWith("image/")) {
1616
+ media = await this.processImageAttachment(attachment);
1617
+ } else if (attachment.contentType?.startsWith("video/") || this.runtime.getService(ServiceTypes3.VIDEO).isVideoUrl(attachment.url)) {
1618
+ media = await this.processVideoAttachment(attachment);
1619
+ } else {
1620
+ media = await this.processGenericAttachment(attachment);
1621
+ }
1622
+ if (media) {
1623
+ this.attachmentCache.set(attachment.url, media);
1624
+ }
1625
+ return media;
1626
+ }
1627
+ async processAudioVideoAttachment(attachment) {
1628
+ try {
1629
+ const response = await fetch(attachment.url);
1630
+ const audioVideoArrayBuffer = await response.arrayBuffer();
1631
+ let audioBuffer;
1632
+ if (attachment.contentType?.startsWith("audio/")) {
1633
+ audioBuffer = Buffer.from(audioVideoArrayBuffer);
1634
+ } else if (attachment.contentType?.startsWith("video/mp4")) {
1635
+ audioBuffer = await this.extractAudioFromMP4(audioVideoArrayBuffer);
1636
+ } else {
1637
+ throw new Error("Unsupported audio/video format");
1638
+ }
1639
+ const transcription = await this.runtime.useModel(
1640
+ ModelTypes6.TRANSCRIPTION,
1641
+ audioBuffer
1642
+ );
1643
+ const { title, description } = await generateSummary(
1644
+ this.runtime,
1645
+ transcription
1646
+ );
1647
+ return {
1648
+ id: attachment.id,
1649
+ url: attachment.url,
1650
+ title: title || "Audio/Video Attachment",
1651
+ source: attachment.contentType?.startsWith("audio/") ? "Audio" : "Video",
1652
+ description: description || "User-uploaded audio/video attachment which has been transcribed",
1653
+ text: transcription || "Audio/video content not available"
1654
+ };
1655
+ } catch (error) {
1656
+ console.error(
1657
+ `Error processing audio/video attachment: ${error.message}`
1658
+ );
1659
+ return {
1660
+ id: attachment.id,
1661
+ url: attachment.url,
1662
+ title: "Audio/Video Attachment",
1663
+ source: attachment.contentType?.startsWith("audio/") ? "Audio" : "Video",
1664
+ description: "An audio/video attachment (transcription failed)",
1665
+ text: `This is an audio/video attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
1666
+ };
1667
+ }
1668
+ }
1669
+ async extractAudioFromMP4(mp4Data) {
1670
+ const tempMP4File = `temp_${Date.now()}.mp4`;
1671
+ const tempAudioFile = `temp_${Date.now()}.mp3`;
1672
+ try {
1673
+ fs3.writeFileSync(tempMP4File, Buffer.from(mp4Data));
1674
+ await new Promise((resolve, reject) => {
1675
+ ffmpeg(tempMP4File).outputOptions("-vn").audioCodec("libmp3lame").save(tempAudioFile).on("end", () => {
1676
+ resolve();
1677
+ }).on("error", (err) => {
1678
+ reject(err);
1679
+ }).run();
1680
+ });
1681
+ const audioData = fs3.readFileSync(tempAudioFile);
1682
+ return audioData;
1683
+ } finally {
1684
+ if (fs3.existsSync(tempMP4File)) {
1685
+ fs3.unlinkSync(tempMP4File);
1686
+ }
1687
+ if (fs3.existsSync(tempAudioFile)) {
1688
+ fs3.unlinkSync(tempAudioFile);
1689
+ }
1690
+ }
1691
+ }
1692
+ async processPdfAttachment(attachment) {
1693
+ try {
1694
+ const response = await fetch(attachment.url);
1695
+ const pdfBuffer = await response.arrayBuffer();
1696
+ const text = await this.runtime.getService(ServiceTypes3.PDF).convertPdfToText(Buffer.from(pdfBuffer));
1697
+ const { title, description } = await generateSummary(this.runtime, text);
1698
+ return {
1699
+ id: attachment.id,
1700
+ url: attachment.url,
1701
+ title: title || "PDF Attachment",
1702
+ source: "PDF",
1703
+ description: description || "A PDF document",
1704
+ text
1705
+ };
1706
+ } catch (error) {
1707
+ console.error(`Error processing PDF attachment: ${error.message}`);
1708
+ return {
1709
+ id: attachment.id,
1710
+ url: attachment.url,
1711
+ title: "PDF Attachment (conversion failed)",
1712
+ source: "PDF",
1713
+ description: "A PDF document that could not be converted to text",
1714
+ text: `This is a PDF attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes`
1715
+ };
1716
+ }
1717
+ }
1718
+ async processPlaintextAttachment(attachment) {
1719
+ try {
1720
+ const response = await fetch(attachment.url);
1721
+ const text = await response.text();
1722
+ const { title, description } = await generateSummary(this.runtime, text);
1723
+ return {
1724
+ id: attachment.id,
1725
+ url: attachment.url,
1726
+ title: title || "Plaintext Attachment",
1727
+ source: "Plaintext",
1728
+ description: description || "A plaintext document",
1729
+ text
1730
+ };
1731
+ } catch (error) {
1732
+ console.error(`Error processing plaintext attachment: ${error.message}`);
1733
+ return {
1734
+ id: attachment.id,
1735
+ url: attachment.url,
1736
+ title: "Plaintext Attachment (retrieval failed)",
1737
+ source: "Plaintext",
1738
+ description: "A plaintext document that could not be retrieved",
1739
+ text: `This is a plaintext attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes`
1740
+ };
1741
+ }
1742
+ }
1743
+ async processImageAttachment(attachment) {
1744
+ try {
1745
+ const { description, title } = await this.runtime.useModel(
1746
+ ModelTypes6.IMAGE_DESCRIPTION,
1747
+ attachment.url
1748
+ );
1749
+ return {
1750
+ id: attachment.id,
1751
+ url: attachment.url,
1752
+ title: title || "Image Attachment",
1753
+ source: "Image",
1754
+ description: description || "An image attachment",
1755
+ text: description || "Image content not available"
1756
+ };
1757
+ } catch (error) {
1758
+ console.error(`Error processing image attachment: ${error.message}`);
1759
+ return this.createFallbackImageMedia(attachment);
1760
+ }
1761
+ }
1762
+ createFallbackImageMedia(attachment) {
1763
+ return {
1764
+ id: attachment.id,
1765
+ url: attachment.url,
1766
+ title: "Image Attachment",
1767
+ source: "Image",
1768
+ description: "An image attachment (recognition failed)",
1769
+ text: `This is an image attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
1770
+ };
1771
+ }
1772
+ async processVideoAttachment(attachment) {
1773
+ const videoService = this.runtime.getService(
1774
+ ServiceTypes3.VIDEO
1775
+ );
1776
+ if (!videoService) {
1777
+ throw new Error("Video service not found");
1778
+ }
1779
+ if (videoService.isVideoUrl(attachment.url)) {
1780
+ const videoInfo = await videoService.processVideo(
1781
+ attachment.url,
1782
+ this.runtime
1783
+ );
1784
+ return {
1785
+ id: attachment.id,
1786
+ url: attachment.url,
1787
+ title: videoInfo.title,
1788
+ source: "YouTube",
1789
+ description: videoInfo.description,
1790
+ text: videoInfo.text
1791
+ };
1792
+ }
1793
+ return {
1794
+ id: attachment.id,
1795
+ url: attachment.url,
1796
+ title: "Video Attachment",
1797
+ source: "Video",
1798
+ description: "A video attachment",
1799
+ text: "Video content not available"
1800
+ };
1801
+ }
1802
+ async processGenericAttachment(attachment) {
1803
+ return {
1804
+ id: attachment.id,
1805
+ url: attachment.url,
1806
+ title: "Generic Attachment",
1807
+ source: "Generic",
1808
+ description: "A generic attachment",
1809
+ text: "Attachment content not available"
1810
+ };
1811
+ }
1812
+ };
1813
+
1814
+ // src/utils.ts
1815
+ import {
1816
+ ModelTypes as ModelTypes7,
1817
+ logger as logger3,
1818
+ trimTokens as trimTokens4,
1819
+ parseJSONObjectFromText as parseJSONObjectFromText6
1820
+ } from "@elizaos/core";
1821
+ import {
1822
+ ChannelType as ChannelType4,
1823
+ PermissionsBitField,
1824
+ ThreadChannel
1825
+ } from "discord.js";
1826
+ function getWavHeader(audioLength, sampleRate, channelCount = 1, bitsPerSample = 16) {
1827
+ const wavHeader = Buffer.alloc(44);
1828
+ wavHeader.write("RIFF", 0);
1829
+ wavHeader.writeUInt32LE(36 + audioLength, 4);
1830
+ wavHeader.write("WAVE", 8);
1831
+ wavHeader.write("fmt ", 12);
1832
+ wavHeader.writeUInt32LE(16, 16);
1833
+ wavHeader.writeUInt16LE(1, 20);
1834
+ wavHeader.writeUInt16LE(channelCount, 22);
1835
+ wavHeader.writeUInt32LE(sampleRate, 24);
1836
+ wavHeader.writeUInt32LE(sampleRate * bitsPerSample * channelCount / 8, 28);
1837
+ wavHeader.writeUInt16LE(bitsPerSample * channelCount / 8, 32);
1838
+ wavHeader.writeUInt16LE(bitsPerSample, 34);
1839
+ wavHeader.write("data", 36);
1840
+ wavHeader.writeUInt32LE(audioLength, 40);
1841
+ return wavHeader;
1842
+ }
1843
+ var MAX_MESSAGE_LENGTH = 1900;
1844
+ async function sendMessageInChunks(channel, content, _inReplyTo, files) {
1845
+ const sentMessages = [];
1846
+ const messages = splitMessage(content);
1847
+ try {
1848
+ for (let i = 0; i < messages.length; i++) {
1849
+ const message = messages[i];
1850
+ if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0) {
1851
+ const options = {
1852
+ content: message.trim()
1853
+ };
1854
+ if (i === messages.length - 1 && files && files.length > 0) {
1855
+ options.files = files;
1856
+ }
1857
+ const m = await channel.send(options);
1858
+ sentMessages.push(m);
1859
+ }
1860
+ }
1861
+ } catch (error) {
1862
+ logger3.error("Error sending message:", error);
1863
+ }
1864
+ return sentMessages;
1865
+ }
1866
+ function splitMessage(content) {
1867
+ const messages = [];
1868
+ let currentMessage = "";
1869
+ const rawLines = content?.split("\n") || [];
1870
+ const lines = rawLines.flatMap((line) => {
1871
+ const chunks = [];
1872
+ while (line.length > MAX_MESSAGE_LENGTH) {
1873
+ chunks.push(line.slice(0, MAX_MESSAGE_LENGTH));
1874
+ line = line.slice(MAX_MESSAGE_LENGTH);
1875
+ }
1876
+ chunks.push(line);
1877
+ return chunks;
1878
+ });
1879
+ for (const line of lines) {
1880
+ if (currentMessage.length + line.length + 1 > MAX_MESSAGE_LENGTH) {
1881
+ messages.push(currentMessage.trim());
1882
+ currentMessage = "";
1883
+ }
1884
+ currentMessage += `${line}
1885
+ `;
1886
+ }
1887
+ if (currentMessage.trim().length > 0) {
1888
+ messages.push(currentMessage.trim());
1889
+ }
1890
+ return messages;
1891
+ }
1892
+ function canSendMessage(channel) {
1893
+ if (!channel) {
1894
+ return {
1895
+ canSend: false,
1896
+ reason: "No channel given"
1897
+ };
1898
+ }
1899
+ if (channel.type === ChannelType4.DM) {
1900
+ return {
1901
+ canSend: true,
1902
+ reason: null
1903
+ };
1904
+ }
1905
+ const botMember = channel.guild?.members.cache.get(channel.client.user.id);
1906
+ if (!botMember) {
1907
+ return {
1908
+ canSend: false,
1909
+ reason: "Not a guild channel or bot member not found"
1910
+ };
1911
+ }
1912
+ const requiredPermissions = [
1913
+ PermissionsBitField.Flags.ViewChannel,
1914
+ PermissionsBitField.Flags.SendMessages,
1915
+ PermissionsBitField.Flags.ReadMessageHistory
1916
+ ];
1917
+ if (channel instanceof ThreadChannel) {
1918
+ requiredPermissions.push(PermissionsBitField.Flags.SendMessagesInThreads);
1919
+ }
1920
+ const permissions = channel.permissionsFor(botMember);
1921
+ if (!permissions) {
1922
+ return {
1923
+ canSend: false,
1924
+ reason: "Could not retrieve permissions"
1925
+ };
1926
+ }
1927
+ const missingPermissions = requiredPermissions.filter(
1928
+ (perm) => !permissions.has(perm)
1929
+ );
1930
+ return {
1931
+ canSend: missingPermissions.length === 0,
1932
+ missingPermissions,
1933
+ reason: missingPermissions.length > 0 ? `Missing permissions: ${missingPermissions.map((p) => String(p)).join(", ")}` : null
1934
+ };
1935
+ }
1936
+
1937
+ // src/messages.ts
1938
+ var MessageManager = class {
1939
+ client;
1940
+ runtime;
1941
+ attachmentManager;
1942
+ getChannelType;
1943
+ constructor(discordClient) {
1944
+ this.client = discordClient.client;
1945
+ this.runtime = discordClient.runtime;
1946
+ this.attachmentManager = new AttachmentManager(this.runtime);
1947
+ this.getChannelType = discordClient.getChannelType;
1948
+ }
1949
+ async handleMessage(message) {
1950
+ if (this.runtime.character.settings?.discord?.allowedChannelIds && !this.runtime.character.settings.discord.allowedChannelIds.some(
1951
+ (id) => id === message.channel.id
1952
+ )) {
1953
+ return;
1954
+ }
1955
+ if (message.interaction || message.author.id === this.client.user?.id) {
1956
+ return;
1957
+ }
1958
+ if (this.runtime.character.settings?.discord?.shouldIgnoreBotMessages && message.author?.bot) {
1959
+ return;
1960
+ }
1961
+ if (this.runtime.character.settings?.discord?.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType2.DM) {
1962
+ return;
1963
+ }
1964
+ const entityId = createUniqueUuid4(this.runtime, message.author.id);
1965
+ const userName = message.author.bot ? `${message.author.username}#${message.author.discriminator}` : message.author.username;
1966
+ const name = message.author.displayName;
1967
+ const channelId = message.channel.id;
1968
+ const roomId = createUniqueUuid4(this.runtime, channelId);
1969
+ let type;
1970
+ let serverId;
1971
+ if (message.guild) {
1972
+ const guild = await message.guild.fetch();
1973
+ type = await this.getChannelType(message.channel);
1974
+ serverId = guild.id;
1975
+ } else {
1976
+ type = ChannelType5.DM;
1977
+ serverId = void 0;
1978
+ }
1979
+ await this.runtime.ensureConnection({
1980
+ entityId,
1981
+ roomId,
1982
+ userName,
1983
+ name,
1984
+ source: "discord",
1985
+ channelId: message.channel.id,
1986
+ serverId,
1987
+ type
1988
+ });
1989
+ try {
1990
+ const canSendResult = canSendMessage(message.channel);
1991
+ if (!canSendResult.canSend) {
1992
+ return logger4.warn(
1993
+ `Cannot send message to channel ${message.channel}`,
1994
+ canSendResult
1995
+ );
1996
+ }
1997
+ const { processedContent, attachments } = await this.processMessage(message);
1998
+ const audioAttachments = message.attachments.filter(
1999
+ (attachment) => attachment.contentType?.startsWith("audio/")
2000
+ );
2001
+ if (audioAttachments.size > 0) {
2002
+ const processedAudioAttachments = await this.attachmentManager.processAttachments(audioAttachments);
2003
+ attachments.push(...processedAudioAttachments);
2004
+ }
2005
+ if (!processedContent && !attachments?.length) {
2006
+ return;
2007
+ }
2008
+ const entityId2 = createUniqueUuid4(this.runtime, message.author.id);
2009
+ const messageId = createUniqueUuid4(this.runtime, message.id);
2010
+ const newMessage = {
2011
+ id: messageId,
2012
+ entityId: entityId2,
2013
+ agentId: this.runtime.agentId,
2014
+ roomId,
2015
+ content: {
2016
+ // name: name,
2017
+ // userName: userName,
2018
+ text: processedContent || " ",
2019
+ attachments,
2020
+ source: "discord",
2021
+ url: message.url,
2022
+ inReplyTo: message.reference?.messageId ? createUniqueUuid4(this.runtime, message.reference?.messageId) : void 0
2023
+ },
2024
+ createdAt: message.createdTimestamp
2025
+ };
2026
+ const callback = async (content, files) => {
2027
+ try {
2028
+ if (message.id && !content.inReplyTo) {
2029
+ content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
2030
+ }
2031
+ const messages = await sendMessageInChunks(
2032
+ message.channel,
2033
+ content.text,
2034
+ message.id,
2035
+ files
2036
+ );
2037
+ const memories = [];
2038
+ for (const m of messages) {
2039
+ const actions = content.actions;
2040
+ const memory = {
2041
+ id: createUniqueUuid4(this.runtime, m.id),
2042
+ entityId: this.runtime.agentId,
2043
+ agentId: this.runtime.agentId,
2044
+ content: {
2045
+ ...content,
2046
+ actions,
2047
+ inReplyTo: messageId,
2048
+ url: m.url,
2049
+ channelType: type
2050
+ },
2051
+ roomId,
2052
+ createdAt: m.createdTimestamp
2053
+ };
2054
+ memories.push(memory);
2055
+ }
2056
+ for (const m of memories) {
2057
+ await this.runtime.getMemoryManager("messages").createMemory(m);
2058
+ }
2059
+ return memories;
2060
+ } catch (error) {
2061
+ console.error("Error sending message:", error);
2062
+ return [];
2063
+ }
2064
+ };
2065
+ this.runtime.emitEvent(["DISCORD_MESSAGE_RECEIVED", "MESSAGE_RECEIVED"], {
2066
+ runtime: this.runtime,
2067
+ message: newMessage,
2068
+ callback
2069
+ });
2070
+ } catch (error) {
2071
+ console.error("Error handling message:", error);
2072
+ }
2073
+ }
2074
+ async processMessage(message) {
2075
+ let processedContent = message.content;
2076
+ let attachments = [];
2077
+ const mentionRegex = /<@!?(\d+)>/g;
2078
+ processedContent = processedContent.replace(
2079
+ mentionRegex,
2080
+ (match2, entityId) => {
2081
+ const user = message.mentions.users.get(entityId);
2082
+ if (user) {
2083
+ return `${user.username} (@${entityId})`;
2084
+ }
2085
+ return match2;
2086
+ }
2087
+ );
2088
+ const codeBlockRegex = /```([\s\S]*?)```/g;
2089
+ let match;
2090
+ while (match = codeBlockRegex.exec(processedContent)) {
2091
+ const codeBlock = match[1];
2092
+ const lines = codeBlock.split("\n");
2093
+ const title = lines[0];
2094
+ const description = lines.slice(0, 3).join("\n");
2095
+ const attachmentId = `code-${Date.now()}-${Math.floor(
2096
+ Math.random() * 1e3
2097
+ )}`.slice(-5);
2098
+ attachments.push({
2099
+ id: attachmentId,
2100
+ url: "",
2101
+ title: title || "Code Block",
2102
+ source: "Code",
2103
+ description,
2104
+ text: codeBlock
2105
+ });
2106
+ processedContent = processedContent.replace(
2107
+ match[0],
2108
+ `Code Block (${attachmentId})`
2109
+ );
2110
+ }
2111
+ if (message.attachments.size > 0) {
2112
+ attachments = await this.attachmentManager.processAttachments(
2113
+ message.attachments
2114
+ );
2115
+ }
2116
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
2117
+ const urls = processedContent.match(urlRegex) || [];
2118
+ for (const url of urls) {
2119
+ if (this.runtime.getService(ServiceTypes4.VIDEO)?.isVideoUrl(url)) {
2120
+ const videoService = this.runtime.getService(
2121
+ ServiceTypes4.VIDEO
2122
+ );
2123
+ if (!videoService) {
2124
+ throw new Error("Video service not found");
2125
+ }
2126
+ const videoInfo = await videoService.processVideo(url, this.runtime);
2127
+ attachments.push({
2128
+ id: `youtube-${Date.now()}`,
2129
+ url,
2130
+ title: videoInfo.title,
2131
+ source: "YouTube",
2132
+ description: videoInfo.description,
2133
+ text: videoInfo.text
2134
+ });
2135
+ } else {
2136
+ const browserService = this.runtime.getService(
2137
+ ServiceTypes4.BROWSER
2138
+ );
2139
+ if (!browserService) {
2140
+ throw new Error("Browser service not found");
2141
+ }
2142
+ const { title, description: summary } = await browserService.getPageContent(url, this.runtime);
2143
+ attachments.push({
2144
+ id: `webpage-${Date.now()}`,
2145
+ url,
2146
+ title: title || "Web Page",
2147
+ source: "Web",
2148
+ description: summary,
2149
+ text: summary
2150
+ });
2151
+ }
2152
+ }
2153
+ return { processedContent, attachments };
2154
+ }
2155
+ async fetchBotName(botToken) {
2156
+ const url = "https://discord.com/api/v10/users/@me";
2157
+ const response = await fetch(url, {
2158
+ method: "GET",
2159
+ headers: {
2160
+ Authorization: `Bot ${botToken}`
2161
+ }
2162
+ });
2163
+ if (!response.ok) {
2164
+ throw new Error(`Error fetching bot details: ${response.statusText}`);
2165
+ }
2166
+ const data = await response.json();
2167
+ const discriminator = data.discriminator;
2168
+ return data.username + (discriminator ? `#${discriminator}` : "");
2169
+ }
2170
+ };
2171
+
2172
+ // src/providers/channelState.ts
2173
+ import { ChannelType as ChannelType6 } from "@elizaos/core";
2174
+ var channelStateProvider = {
2175
+ name: "channelState",
2176
+ get: async (runtime, message, state) => {
2177
+ const room = state.data?.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
2178
+ if (!room) {
2179
+ throw new Error("No room found");
2180
+ }
2181
+ if (message.content.source !== "discord") {
2182
+ return {
2183
+ data: null,
2184
+ values: {},
2185
+ text: ""
2186
+ };
2187
+ }
2188
+ const agentName = state?.agentName || "The agent";
2189
+ const senderName = state?.senderName || "someone";
2190
+ let responseText = "";
2191
+ let channelType = "";
2192
+ let serverName = "";
2193
+ let channelId = "";
2194
+ const serverId = room.serverId;
2195
+ if (room.type === ChannelType6.DM) {
2196
+ channelType = "DM";
2197
+ responseText = `${agentName} is currently in a direct message conversation with ${senderName}. ${agentName} should engage in conversation, should respond to messages that are addressed to them and only ignore messages that seem to not require a response.`;
2198
+ } else {
2199
+ channelType = "GROUP";
2200
+ if (!serverId) {
2201
+ console.error("No server ID found");
2202
+ return {
2203
+ data: {
2204
+ room,
2205
+ channelType
2206
+ },
2207
+ values: {
2208
+ channelType
2209
+ },
2210
+ text: ""
2211
+ };
2212
+ }
2213
+ channelId = room.channelId;
2214
+ const discordService = runtime.getService(
2215
+ ServiceTypes2.DISCORD
2216
+ );
2217
+ if (!discordService) {
2218
+ console.warn("No discord client found");
2219
+ return {
2220
+ data: {
2221
+ room,
2222
+ channelType,
2223
+ serverId
2224
+ },
2225
+ values: {
2226
+ channelType,
2227
+ serverId
2228
+ },
2229
+ text: ""
2230
+ };
2231
+ }
2232
+ const guild = discordService.client.guilds.cache.get(serverId);
2233
+ serverName = guild.name;
2234
+ responseText = `${agentName} is currently having a conversation in the channel \`@${channelId} in the server \`${serverName}\` (@${serverId})`;
2235
+ responseText += `
2236
+ ${agentName} is in a room with other users and should be self-conscious and only participate when directly addressed or when the conversation is relevant to them.`;
2237
+ }
2238
+ return {
2239
+ data: {
2240
+ room,
2241
+ channelType,
2242
+ serverId,
2243
+ serverName,
2244
+ channelId
2245
+ },
2246
+ values: {
2247
+ channelType,
2248
+ serverName,
2249
+ channelId
2250
+ },
2251
+ text: responseText
2252
+ };
2253
+ }
2254
+ };
2255
+ var channelState_default = channelStateProvider;
2256
+
2257
+ // src/providers/voiceState.ts
2258
+ import { getVoiceConnection } from "@discordjs/voice";
2259
+ import { ChannelType as ChannelType7 } from "@elizaos/core";
2260
+ var voiceStateProvider = {
2261
+ name: "voiceState",
2262
+ get: async (runtime, message, state) => {
2263
+ const room = await runtime.getDatabaseAdapter().getRoom(message.roomId);
2264
+ if (!room) {
2265
+ throw new Error("No room found");
2266
+ }
2267
+ if (room.type !== ChannelType7.GROUP) {
2268
+ return {
2269
+ data: {
2270
+ isInVoiceChannel: false,
2271
+ room
2272
+ },
2273
+ values: {
2274
+ isInVoiceChannel: "false",
2275
+ roomType: room.type
2276
+ },
2277
+ text: ""
2278
+ };
2279
+ }
2280
+ const serverId = room.serverId;
2281
+ if (!serverId) {
2282
+ throw new Error("No server ID found 10");
2283
+ }
2284
+ const connection = getVoiceConnection(serverId);
2285
+ const agentName = state?.agentName || "The agent";
2286
+ if (!connection) {
2287
+ return {
2288
+ data: {
2289
+ isInVoiceChannel: false,
2290
+ room,
2291
+ serverId
2292
+ },
2293
+ values: {
2294
+ isInVoiceChannel: "false",
2295
+ serverId
2296
+ },
2297
+ text: `${agentName} is not currently in a voice channel`
2298
+ };
2299
+ }
2300
+ const worldId = room.worldId;
2301
+ const world = await runtime.getDatabaseAdapter().getWorld(worldId);
2302
+ if (!world) {
2303
+ throw new Error("No world found");
2304
+ }
2305
+ const worldName = world.name;
2306
+ const roomType = room.type;
2307
+ const channelId = room.channelId;
2308
+ const channelName = room.name;
2309
+ if (!channelId) {
2310
+ return {
2311
+ data: {
2312
+ isInVoiceChannel: true,
2313
+ room,
2314
+ serverId,
2315
+ world,
2316
+ connection
2317
+ },
2318
+ values: {
2319
+ isInVoiceChannel: "true",
2320
+ serverId,
2321
+ worldName,
2322
+ roomType
2323
+ },
2324
+ text: `${agentName} is in an invalid voice channel`
2325
+ };
2326
+ }
2327
+ return {
2328
+ data: {
2329
+ isInVoiceChannel: true,
2330
+ room,
2331
+ serverId,
2332
+ world,
2333
+ connection,
2334
+ channelId,
2335
+ channelName
2336
+ },
2337
+ values: {
2338
+ isInVoiceChannel: "true",
2339
+ serverId,
2340
+ worldName,
2341
+ roomType,
2342
+ channelId,
2343
+ channelName
2344
+ },
2345
+ text: `${agentName} is currently in the voice channel: ${channelName} (ID: ${channelId})`
2346
+ };
2347
+ }
2348
+ };
2349
+ var voiceState_default = voiceStateProvider;
2350
+
2351
+ // src/tests.ts
2352
+ import {
2353
+ AudioPlayerStatus,
2354
+ NoSubscriberBehavior,
2355
+ VoiceConnectionStatus,
2356
+ createAudioPlayer,
2357
+ createAudioResource,
2358
+ entersState
2359
+ } from "@discordjs/voice";
2360
+ import {
2361
+ ModelTypes as ModelTypes8,
2362
+ logger as logger5
2363
+ } from "@elizaos/core";
2364
+ import { ChannelType as ChannelType8, Events } from "discord.js";
2365
+ var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
2366
+ var DiscordTestSuite = class {
2367
+ name = "discord";
2368
+ discordClient = null;
2369
+ tests;
2370
+ constructor() {
2371
+ this.tests = [
2372
+ {
2373
+ name: "Initialize Discord Client",
2374
+ fn: this.testCreatingDiscordClient.bind(this)
2375
+ },
2376
+ {
2377
+ name: "Slash Commands - Join Voice",
2378
+ fn: this.testJoinVoiceSlashCommand.bind(this)
2379
+ },
2380
+ {
2381
+ name: "Voice Playback & TTS",
2382
+ fn: this.testTextToSpeechPlayback.bind(this)
2383
+ },
2384
+ {
2385
+ name: "Send Message with Attachments",
2386
+ fn: this.testSendingTextMessage.bind(this)
2387
+ },
2388
+ {
2389
+ name: "Handle Incoming Messages",
2390
+ fn: this.testHandlingMessage.bind(this)
2391
+ },
2392
+ {
2393
+ name: "Slash Commands - Leave Voice",
2394
+ fn: this.testLeaveVoiceSlashCommand.bind(this)
2395
+ }
2396
+ ];
2397
+ }
2398
+ async testCreatingDiscordClient(runtime) {
2399
+ try {
2400
+ this.discordClient = runtime.getService(
2401
+ ServiceTypes2.DISCORD
2402
+ );
2403
+ if (this.discordClient.client.isReady()) {
2404
+ logger5.success("DiscordService is already ready.");
2405
+ } else {
2406
+ logger5.info("Waiting for DiscordService to be ready...");
2407
+ await new Promise((resolve, reject) => {
2408
+ this.discordClient.client.once(Events.ClientReady, resolve);
2409
+ this.discordClient.client.once(Events.Error, reject);
2410
+ });
2411
+ }
2412
+ } catch (error) {
2413
+ throw new Error(`Error in test creating Discord client: ${error}`);
2414
+ }
2415
+ }
2416
+ async testJoinVoiceSlashCommand(runtime) {
2417
+ try {
2418
+ await this.waitForVoiceManagerReady(this.discordClient);
2419
+ const channel = await this.getTestChannel(runtime);
2420
+ if (!channel || !channel.isTextBased()) {
2421
+ throw new Error("Invalid test channel for slash command test.");
2422
+ }
2423
+ const fakeJoinInteraction = {
2424
+ isCommand: () => true,
2425
+ commandName: "joinchannel",
2426
+ options: {
2427
+ get: (name) => name === "channel" ? { value: channel.id } : null
2428
+ },
2429
+ guild: channel.guild,
2430
+ deferReply: async () => {
2431
+ },
2432
+ editReply: async (message) => {
2433
+ logger5.info(`JoinChannel Slash Command Response: ${message}`);
2434
+ }
2435
+ };
2436
+ await this.discordClient.voiceManager.handleJoinChannelCommand(
2437
+ fakeJoinInteraction
2438
+ );
2439
+ logger5.success("Slash command test completed successfully.");
2440
+ } catch (error) {
2441
+ throw new Error(`Error in slash commands test: ${error}`);
2442
+ }
2443
+ }
2444
+ async testLeaveVoiceSlashCommand(runtime) {
2445
+ try {
2446
+ await this.waitForVoiceManagerReady(this.discordClient);
2447
+ const channel = await this.getTestChannel(runtime);
2448
+ if (!channel || !channel.isTextBased()) {
2449
+ throw new Error("Invalid test channel for slash command test.");
2450
+ }
2451
+ const fakeLeaveInteraction = {
2452
+ isCommand: () => true,
2453
+ commandName: "leavechannel",
2454
+ guildId: channel.guildId,
2455
+ reply: async (message) => {
2456
+ logger5.info(`LeaveChannel Slash Command Response: ${message}`);
2457
+ }
2458
+ };
2459
+ await this.discordClient.voiceManager.handleLeaveChannelCommand(
2460
+ fakeLeaveInteraction
2461
+ );
2462
+ logger5.success("Slash command test completed successfully.");
2463
+ } catch (error) {
2464
+ throw new Error(`Error in slash commands test: ${error}`);
2465
+ }
2466
+ }
2467
+ async testTextToSpeechPlayback(runtime) {
2468
+ try {
2469
+ await this.waitForVoiceManagerReady(this.discordClient);
2470
+ const channel = await this.getTestChannel(runtime);
2471
+ if (!channel || channel.type !== ChannelType8.GuildVoice) {
2472
+ throw new Error("Invalid voice channel.");
2473
+ }
2474
+ await this.discordClient.voiceManager.joinChannel(channel);
2475
+ const guild = await this.getActiveGuild(this.discordClient);
2476
+ const guildId = guild.id;
2477
+ const connection = this.discordClient.voiceManager.getVoiceConnection(guildId);
2478
+ try {
2479
+ await entersState(connection, VoiceConnectionStatus.Ready, 1e4);
2480
+ logger5.success(`Voice connection is ready in guild: ${guildId}`);
2481
+ } catch (error) {
2482
+ throw new Error(`Voice connection failed to become ready: ${error}`);
2483
+ }
2484
+ let responseStream = null;
2485
+ try {
2486
+ responseStream = await runtime.useModel(
2487
+ ModelTypes8.TEXT_TO_SPEECH,
2488
+ `Hi! I'm ${runtime.character.name}! How are you doing today?`
2489
+ );
2490
+ } catch (_error) {
2491
+ throw new Error("No text to speech service found");
2492
+ }
2493
+ if (!responseStream) {
2494
+ throw new Error("TTS response stream is null or undefined.");
2495
+ }
2496
+ await this.playAudioStream(responseStream, connection);
2497
+ } catch (error) {
2498
+ throw new Error(`Error in TTS playback test: ${error}`);
2499
+ }
2500
+ }
2501
+ async testSendingTextMessage(runtime) {
2502
+ try {
2503
+ const channel = await this.getTestChannel(runtime);
2504
+ await this.sendMessageToChannel(
2505
+ channel,
2506
+ "Testing Message",
2507
+ [TEST_IMAGE_URL]
2508
+ );
2509
+ } catch (error) {
2510
+ throw new Error(`Error in sending text message: ${error}`);
2511
+ }
2512
+ }
2513
+ async testHandlingMessage(runtime) {
2514
+ try {
2515
+ const channel = await this.getTestChannel(runtime);
2516
+ const fakeMessage = {
2517
+ content: `Hello, ${runtime.character.name}! How are you?`,
2518
+ author: {
2519
+ id: "mock-user-id",
2520
+ username: "MockUser",
2521
+ bot: false
2522
+ },
2523
+ channel,
2524
+ id: "mock-message-id",
2525
+ createdTimestamp: Date.now(),
2526
+ mentions: {
2527
+ has: () => false
2528
+ },
2529
+ reference: null,
2530
+ attachments: []
2531
+ };
2532
+ await this.discordClient.messageManager.handleMessage(fakeMessage);
2533
+ } catch (error) {
2534
+ throw new Error(`Error in sending text message: ${error}`);
2535
+ }
2536
+ }
2537
+ // #############################
2538
+ // Utility Functions
2539
+ // #############################
2540
+ async getTestChannel(runtime) {
2541
+ const channelId = this.validateChannelId(runtime);
2542
+ const channel = await this.discordClient.client.channels.fetch(channelId);
2543
+ if (!channel) throw new Error("no test channel found!");
2544
+ return channel;
2545
+ }
2546
+ async sendMessageToChannel(channel, messageContent, files) {
2547
+ try {
2548
+ if (!channel || !channel.isTextBased()) {
2549
+ throw new Error(
2550
+ "Channel is not a text-based channel or does not exist."
2551
+ );
2552
+ }
2553
+ await sendMessageInChunks(
2554
+ channel,
2555
+ messageContent,
2556
+ null,
2557
+ files
2558
+ );
2559
+ } catch (error) {
2560
+ throw new Error(`Error sending message: ${error}`);
2561
+ }
2562
+ }
2563
+ async playAudioStream(responseStream, connection) {
2564
+ const audioPlayer = createAudioPlayer({
2565
+ behaviors: {
2566
+ noSubscriber: NoSubscriberBehavior.Pause
2567
+ }
2568
+ });
2569
+ const audioResource = createAudioResource(responseStream);
2570
+ audioPlayer.play(audioResource);
2571
+ connection.subscribe(audioPlayer);
2572
+ logger5.success("TTS playback started successfully.");
2573
+ await new Promise((resolve, reject) => {
2574
+ audioPlayer.once(AudioPlayerStatus.Idle, () => {
2575
+ logger5.info("TTS playback finished.");
2576
+ resolve();
2577
+ });
2578
+ audioPlayer.once("error", (error) => {
2579
+ reject(error);
2580
+ throw new Error(`TTS playback error: ${error}`);
2581
+ });
2582
+ });
2583
+ }
2584
+ async getActiveGuild(discordClient) {
2585
+ const guilds = await discordClient.client.guilds.fetch();
2586
+ const fullGuilds = await Promise.all(guilds.map((guild) => guild.fetch()));
2587
+ const activeGuild = fullGuilds.find((g) => g.members.me?.voice.channelId);
2588
+ if (!activeGuild) {
2589
+ throw new Error("No active voice connection found for the bot.");
2590
+ }
2591
+ return activeGuild;
2592
+ }
2593
+ async waitForVoiceManagerReady(discordClient) {
2594
+ if (!discordClient) {
2595
+ throw new Error("Discord client is not initialized.");
2596
+ }
2597
+ if (!discordClient.voiceManager.isReady()) {
2598
+ await new Promise((resolve, reject) => {
2599
+ discordClient.voiceManager.once("ready", resolve);
2600
+ discordClient.voiceManager.once("error", reject);
2601
+ });
2602
+ }
2603
+ }
2604
+ validateChannelId(runtime) {
2605
+ const testChannelId = runtime.getSetting("DISCORD_TEST_CHANNEL_ID") || process.env.DISCORD_TEST_CHANNEL_ID;
2606
+ if (!testChannelId) {
2607
+ throw new Error(
2608
+ "DISCORD_TEST_CHANNEL_ID is not set. Please provide a valid channel ID in the environment variables."
2609
+ );
2610
+ }
2611
+ return testChannelId;
2612
+ }
2613
+ };
2614
+
2615
+ // src/voice.ts
2616
+ import {
2617
+ NoSubscriberBehavior as NoSubscriberBehavior2,
2618
+ StreamType,
2619
+ VoiceConnectionStatus as VoiceConnectionStatus2,
2620
+ createAudioPlayer as createAudioPlayer2,
2621
+ createAudioResource as createAudioResource2,
2622
+ entersState as entersState2,
2623
+ getVoiceConnections,
2624
+ joinVoiceChannel
2625
+ } from "@discordjs/voice";
2626
+ import {
2627
+ ChannelType as ChannelType9,
2628
+ ModelTypes as ModelTypes9,
2629
+ createUniqueUuid as createUniqueUuid5,
2630
+ logger as logger6
2631
+ } from "@elizaos/core";
2632
+ import {
2633
+ ChannelType as DiscordChannelType3
2634
+ } from "discord.js";
2635
+ import { EventEmitter } from "node:events";
2636
+ import { pipeline } from "node:stream";
2637
+ import prism from "prism-media";
2638
+ var DECODE_FRAME_SIZE = 1024;
2639
+ var DECODE_SAMPLE_RATE = 16e3;
2640
+ var AudioMonitor = class {
2641
+ readable;
2642
+ buffers = [];
2643
+ maxSize;
2644
+ lastFlagged = -1;
2645
+ ended = false;
2646
+ constructor(readable, maxSize, onStart, callback) {
2647
+ this.readable = readable;
2648
+ this.maxSize = maxSize;
2649
+ this.readable.on("data", (chunk) => {
2650
+ if (this.lastFlagged < 0) {
2651
+ this.lastFlagged = this.buffers.length;
2652
+ }
2653
+ this.buffers.push(chunk);
2654
+ const currentSize = this.buffers.reduce(
2655
+ (acc, cur) => acc + cur.length,
2656
+ 0
2657
+ );
2658
+ while (currentSize > this.maxSize) {
2659
+ this.buffers.shift();
2660
+ this.lastFlagged--;
2661
+ }
2662
+ });
2663
+ this.readable.on("end", () => {
2664
+ logger6.log("AudioMonitor ended");
2665
+ this.ended = true;
2666
+ if (this.lastFlagged < 0) return;
2667
+ callback(this.getBufferFromStart());
2668
+ this.lastFlagged = -1;
2669
+ });
2670
+ this.readable.on("speakingStopped", () => {
2671
+ if (this.ended) return;
2672
+ logger6.log("Speaking stopped");
2673
+ if (this.lastFlagged < 0) return;
2674
+ callback(this.getBufferFromStart());
2675
+ });
2676
+ this.readable.on("speakingStarted", () => {
2677
+ if (this.ended) return;
2678
+ onStart();
2679
+ logger6.log("Speaking started");
2680
+ this.reset();
2681
+ });
2682
+ }
2683
+ stop() {
2684
+ this.readable.removeAllListeners("data");
2685
+ this.readable.removeAllListeners("end");
2686
+ this.readable.removeAllListeners("speakingStopped");
2687
+ this.readable.removeAllListeners("speakingStarted");
2688
+ }
2689
+ isFlagged() {
2690
+ return this.lastFlagged >= 0;
2691
+ }
2692
+ getBufferFromFlag() {
2693
+ if (this.lastFlagged < 0) {
2694
+ return null;
2695
+ }
2696
+ const buffer = Buffer.concat(this.buffers.slice(this.lastFlagged));
2697
+ return buffer;
2698
+ }
2699
+ getBufferFromStart() {
2700
+ const buffer = Buffer.concat(this.buffers);
2701
+ return buffer;
2702
+ }
2703
+ reset() {
2704
+ this.buffers = [];
2705
+ this.lastFlagged = -1;
2706
+ }
2707
+ isEnded() {
2708
+ return this.ended;
2709
+ }
2710
+ };
2711
+ var VoiceManager = class extends EventEmitter {
2712
+ processingVoice = false;
2713
+ transcriptionTimeout = null;
2714
+ userStates = /* @__PURE__ */ new Map();
2715
+ activeAudioPlayer = null;
2716
+ client;
2717
+ runtime;
2718
+ streams = /* @__PURE__ */ new Map();
2719
+ connections = /* @__PURE__ */ new Map();
2720
+ activeMonitors = /* @__PURE__ */ new Map();
2721
+ ready;
2722
+ constructor(service, runtime) {
2723
+ super();
2724
+ this.client = service.client;
2725
+ this.runtime = runtime;
2726
+ this.client.on("voiceManagerReady", () => {
2727
+ this.setReady(true);
2728
+ });
2729
+ }
2730
+ async getChannelType(channel) {
2731
+ switch (channel.type) {
2732
+ case DiscordChannelType3.GuildVoice:
2733
+ case DiscordChannelType3.GuildStageVoice:
2734
+ return ChannelType9.VOICE_GROUP;
2735
+ }
2736
+ }
2737
+ setReady(status) {
2738
+ this.ready = status;
2739
+ this.emit("ready");
2740
+ logger6.success(`VoiceManager is now ready: ${this.ready}`);
2741
+ }
2742
+ isReady() {
2743
+ return this.ready;
2744
+ }
2745
+ async handleVoiceStateUpdate(oldState, newState) {
2746
+ const oldChannelId = oldState.channelId;
2747
+ const newChannelId = newState.channelId;
2748
+ const member = newState.member;
2749
+ if (!member) return;
2750
+ if (member.id === this.client.user?.id) {
2751
+ return;
2752
+ }
2753
+ if (oldChannelId === newChannelId) {
2754
+ return;
2755
+ }
2756
+ if (oldChannelId && this.connections.has(oldChannelId)) {
2757
+ this.stopMonitoringMember(member.id);
2758
+ }
2759
+ if (newChannelId && this.connections.has(newChannelId)) {
2760
+ await this.monitorMember(
2761
+ member,
2762
+ newState.channel
2763
+ );
2764
+ }
2765
+ }
2766
+ async joinChannel(channel) {
2767
+ const oldConnection = this.getVoiceConnection(channel.guildId);
2768
+ if (oldConnection) {
2769
+ try {
2770
+ oldConnection.destroy();
2771
+ this.streams.clear();
2772
+ this.activeMonitors.clear();
2773
+ } catch (error) {
2774
+ console.error("Error leaving voice channel:", error);
2775
+ }
2776
+ }
2777
+ const connection = joinVoiceChannel({
2778
+ channelId: channel.id,
2779
+ guildId: channel.guild.id,
2780
+ adapterCreator: channel.guild.voiceAdapterCreator,
2781
+ selfDeaf: false,
2782
+ selfMute: false,
2783
+ group: this.client.user.id
2784
+ });
2785
+ try {
2786
+ await Promise.race([
2787
+ entersState2(connection, VoiceConnectionStatus2.Ready, 2e4),
2788
+ entersState2(connection, VoiceConnectionStatus2.Signalling, 2e4)
2789
+ ]);
2790
+ logger6.log(
2791
+ `Voice connection established in state: ${connection.state.status}`
2792
+ );
2793
+ connection.on("stateChange", async (oldState, newState) => {
2794
+ logger6.log(
2795
+ `Voice connection state changed from ${oldState.status} to ${newState.status}`
2796
+ );
2797
+ if (newState.status === VoiceConnectionStatus2.Disconnected) {
2798
+ logger6.log("Handling disconnection...");
2799
+ try {
2800
+ await Promise.race([
2801
+ entersState2(connection, VoiceConnectionStatus2.Signalling, 5e3),
2802
+ entersState2(connection, VoiceConnectionStatus2.Connecting, 5e3)
2803
+ ]);
2804
+ logger6.log("Reconnecting to channel...");
2805
+ } catch (e) {
2806
+ logger6.log(`Disconnection confirmed - cleaning up...${e}`);
2807
+ connection.destroy();
2808
+ this.connections.delete(channel.id);
2809
+ }
2810
+ } else if (newState.status === VoiceConnectionStatus2.Destroyed) {
2811
+ this.connections.delete(channel.id);
2812
+ } else if (!this.connections.has(channel.id) && (newState.status === VoiceConnectionStatus2.Ready || newState.status === VoiceConnectionStatus2.Signalling)) {
2813
+ this.connections.set(channel.id, connection);
2814
+ }
2815
+ });
2816
+ connection.on("error", (error) => {
2817
+ logger6.log("Voice connection error:", error);
2818
+ logger6.log("Connection error - will attempt to recover...");
2819
+ });
2820
+ this.connections.set(channel.id, connection);
2821
+ const me = channel.guild.members.me;
2822
+ if (me?.voice && me.permissions.has("DeafenMembers")) {
2823
+ try {
2824
+ await me.voice.setDeaf(false);
2825
+ await me.voice.setMute(false);
2826
+ } catch (error) {
2827
+ logger6.log("Failed to modify voice state:", error);
2828
+ }
2829
+ }
2830
+ connection.receiver.speaking.on("start", async (entityId) => {
2831
+ let user = channel.members.get(entityId);
2832
+ if (!user) {
2833
+ try {
2834
+ user = await channel.guild.members.fetch(entityId);
2835
+ } catch (error) {
2836
+ console.error("Failed to fetch user:", error);
2837
+ }
2838
+ }
2839
+ if (user && !user?.user.bot) {
2840
+ this.monitorMember(user, channel);
2841
+ this.streams.get(entityId)?.emit("speakingStarted");
2842
+ }
2843
+ });
2844
+ connection.receiver.speaking.on("end", async (entityId) => {
2845
+ const user = channel.members.get(entityId);
2846
+ if (!user?.user.bot) {
2847
+ this.streams.get(entityId)?.emit("speakingStopped");
2848
+ }
2849
+ });
2850
+ } catch (error) {
2851
+ logger6.log("Failed to establish voice connection:", error);
2852
+ connection.destroy();
2853
+ this.connections.delete(channel.id);
2854
+ throw error;
2855
+ }
2856
+ }
2857
+ getVoiceConnection(guildId) {
2858
+ const connections = getVoiceConnections(this.client.user.id);
2859
+ if (!connections) {
2860
+ return;
2861
+ }
2862
+ const connection = [...connections.values()].find(
2863
+ (connection2) => connection2.joinConfig.guildId === guildId
2864
+ );
2865
+ return connection;
2866
+ }
2867
+ async monitorMember(member, channel) {
2868
+ const entityId = member?.id;
2869
+ const userName = member?.user?.username;
2870
+ const name = member?.user?.displayName;
2871
+ const connection = this.getVoiceConnection(member?.guild?.id);
2872
+ const receiveStream = connection?.receiver.subscribe(entityId, {
2873
+ autoDestroy: true,
2874
+ emitClose: true
2875
+ });
2876
+ if (!receiveStream || receiveStream.readableLength === 0) {
2877
+ return;
2878
+ }
2879
+ const opusDecoder = new prism.opus.Decoder({
2880
+ channels: 1,
2881
+ rate: DECODE_SAMPLE_RATE,
2882
+ frameSize: DECODE_FRAME_SIZE
2883
+ });
2884
+ const volumeBuffer = [];
2885
+ const VOLUME_WINDOW_SIZE = 30;
2886
+ const SPEAKING_THRESHOLD = 0.05;
2887
+ opusDecoder.on("data", (pcmData) => {
2888
+ if (this.activeAudioPlayer) {
2889
+ const samples = new Int16Array(
2890
+ pcmData.buffer,
2891
+ pcmData.byteOffset,
2892
+ pcmData.length / 2
2893
+ );
2894
+ const maxAmplitude = Math.max(...samples.map(Math.abs)) / 32768;
2895
+ volumeBuffer.push(maxAmplitude);
2896
+ if (volumeBuffer.length > VOLUME_WINDOW_SIZE) {
2897
+ volumeBuffer.shift();
2898
+ }
2899
+ const avgVolume = volumeBuffer.reduce((sum, v) => sum + v, 0) / VOLUME_WINDOW_SIZE;
2900
+ if (avgVolume > SPEAKING_THRESHOLD) {
2901
+ volumeBuffer.length = 0;
2902
+ this.cleanupAudioPlayer(this.activeAudioPlayer);
2903
+ this.processingVoice = false;
2904
+ }
2905
+ }
2906
+ });
2907
+ pipeline(
2908
+ receiveStream,
2909
+ opusDecoder,
2910
+ (err) => {
2911
+ if (err) {
2912
+ console.log(`Opus decoding pipeline error: ${err}`);
2913
+ }
2914
+ }
2915
+ );
2916
+ this.streams.set(entityId, opusDecoder);
2917
+ this.connections.set(entityId, connection);
2918
+ opusDecoder.on("error", (err) => {
2919
+ console.log(`Opus decoding error: ${err}`);
2920
+ });
2921
+ const errorHandler = (err) => {
2922
+ console.log(`Opus decoding error: ${err}`);
2923
+ };
2924
+ const streamCloseHandler = () => {
2925
+ console.log(`voice stream from ${member?.displayName} closed`);
2926
+ this.streams.delete(entityId);
2927
+ this.connections.delete(entityId);
2928
+ };
2929
+ const closeHandler = () => {
2930
+ console.log(`Opus decoder for ${member?.displayName} closed`);
2931
+ opusDecoder.removeListener("error", errorHandler);
2932
+ opusDecoder.removeListener("close", closeHandler);
2933
+ receiveStream?.removeListener("close", streamCloseHandler);
2934
+ };
2935
+ opusDecoder.on("error", errorHandler);
2936
+ opusDecoder.on("close", closeHandler);
2937
+ receiveStream?.on("close", streamCloseHandler);
2938
+ this.client.emit(
2939
+ "userStream",
2940
+ entityId,
2941
+ name,
2942
+ userName,
2943
+ channel,
2944
+ opusDecoder
2945
+ );
2946
+ }
2947
+ leaveChannel(channel) {
2948
+ const connection = this.connections.get(channel.id);
2949
+ if (connection) {
2950
+ connection.destroy();
2951
+ this.connections.delete(channel.id);
2952
+ }
2953
+ for (const [memberId, monitorInfo] of this.activeMonitors) {
2954
+ if (monitorInfo.channel.id === channel.id && memberId !== this.client.user?.id) {
2955
+ this.stopMonitoringMember(memberId);
2956
+ }
2957
+ }
2958
+ console.log(`Left voice channel: ${channel.name} (${channel.id})`);
2959
+ }
2960
+ stopMonitoringMember(memberId) {
2961
+ const monitorInfo = this.activeMonitors.get(memberId);
2962
+ if (monitorInfo) {
2963
+ monitorInfo.monitor.stop();
2964
+ this.activeMonitors.delete(memberId);
2965
+ this.streams.delete(memberId);
2966
+ console.log(`Stopped monitoring user ${memberId}`);
2967
+ }
2968
+ }
2969
+ async debouncedProcessTranscription(entityId, name, userName, channel) {
2970
+ const DEBOUNCE_TRANSCRIPTION_THRESHOLD = 1500;
2971
+ if (this.activeAudioPlayer?.state?.status === "idle") {
2972
+ logger6.log("Cleaning up idle audio player.");
2973
+ this.cleanupAudioPlayer(this.activeAudioPlayer);
2974
+ }
2975
+ if (this.activeAudioPlayer || this.processingVoice) {
2976
+ const state = this.userStates.get(entityId);
2977
+ state.buffers.length = 0;
2978
+ state.totalLength = 0;
2979
+ return;
2980
+ }
2981
+ if (this.transcriptionTimeout) {
2982
+ clearTimeout(this.transcriptionTimeout);
2983
+ }
2984
+ this.transcriptionTimeout = setTimeout(async () => {
2985
+ this.processingVoice = true;
2986
+ try {
2987
+ await this.processTranscription(
2988
+ entityId,
2989
+ channel.id,
2990
+ channel,
2991
+ name,
2992
+ userName
2993
+ );
2994
+ this.userStates.forEach((state, _) => {
2995
+ state.buffers.length = 0;
2996
+ state.totalLength = 0;
2997
+ });
2998
+ } finally {
2999
+ this.processingVoice = false;
3000
+ }
3001
+ }, DEBOUNCE_TRANSCRIPTION_THRESHOLD);
3002
+ }
3003
+ async handleUserStream(userId, name, userName, channel, audioStream) {
3004
+ const entityId = createUniqueUuid5(this.runtime, userId);
3005
+ console.log(`Starting audio monitor for user: ${entityId}`);
3006
+ if (!this.userStates.has(entityId)) {
3007
+ this.userStates.set(entityId, {
3008
+ buffers: [],
3009
+ totalLength: 0,
3010
+ lastActive: Date.now(),
3011
+ transcriptionText: ""
3012
+ });
3013
+ }
3014
+ const state = this.userStates.get(entityId);
3015
+ const processBuffer = async (buffer) => {
3016
+ try {
3017
+ state?.buffers.push(buffer);
3018
+ state.totalLength += buffer.length;
3019
+ state.lastActive = Date.now();
3020
+ this.debouncedProcessTranscription(entityId, name, userName, channel);
3021
+ } catch (error) {
3022
+ console.error(`Error processing buffer for user ${entityId}:`, error);
3023
+ }
3024
+ };
3025
+ new AudioMonitor(
3026
+ audioStream,
3027
+ 1e7,
3028
+ () => {
3029
+ if (this.transcriptionTimeout) {
3030
+ clearTimeout(this.transcriptionTimeout);
3031
+ }
3032
+ },
3033
+ async (buffer) => {
3034
+ if (!buffer) {
3035
+ console.error("Received empty buffer");
3036
+ return;
3037
+ }
3038
+ await processBuffer(buffer);
3039
+ }
3040
+ );
3041
+ }
3042
+ async processTranscription(entityId, channelId, channel, name, userName) {
3043
+ const state = this.userStates.get(entityId);
3044
+ if (!state || state.buffers.length === 0) return;
3045
+ try {
3046
+ let isValidTranscription = function(text) {
3047
+ if (!text || text.includes("[BLANK_AUDIO]")) return false;
3048
+ return true;
3049
+ };
3050
+ const inputBuffer = Buffer.concat(state.buffers, state.totalLength);
3051
+ state.buffers.length = 0;
3052
+ state.totalLength = 0;
3053
+ const wavBuffer = await this.convertOpusToWav(inputBuffer);
3054
+ console.log("Starting transcription...");
3055
+ const transcriptionText = await this.runtime.useModel(
3056
+ ModelTypes9.TRANSCRIPTION,
3057
+ wavBuffer
3058
+ );
3059
+ if (transcriptionText && isValidTranscription(transcriptionText)) {
3060
+ state.transcriptionText += transcriptionText;
3061
+ }
3062
+ if (state.transcriptionText.length) {
3063
+ this.cleanupAudioPlayer(this.activeAudioPlayer);
3064
+ const finalText = state.transcriptionText;
3065
+ state.transcriptionText = "";
3066
+ await this.handleMessage(
3067
+ finalText,
3068
+ entityId,
3069
+ channelId,
3070
+ channel,
3071
+ name,
3072
+ userName
3073
+ );
3074
+ }
3075
+ } catch (error) {
3076
+ console.error(`Error transcribing audio for user ${entityId}:`, error);
3077
+ }
3078
+ }
3079
+ async handleMessage(message, entityId, channelId, channel, name, userName) {
3080
+ try {
3081
+ if (!message || message.trim() === "" || message.length < 3) {
3082
+ return { text: "", actions: ["IGNORE"] };
3083
+ }
3084
+ const roomId = createUniqueUuid5(this.runtime, channelId);
3085
+ const type = await this.getChannelType(channel);
3086
+ await this.runtime.ensureConnection({
3087
+ entityId,
3088
+ roomId,
3089
+ userName,
3090
+ name,
3091
+ source: "discord",
3092
+ channelId,
3093
+ serverId: channel.guild.id,
3094
+ type
3095
+ });
3096
+ const memory = {
3097
+ id: createUniqueUuid5(
3098
+ this.runtime,
3099
+ `${channelId}-voice-message-${Date.now()}`
3100
+ ),
3101
+ agentId: this.runtime.agentId,
3102
+ entityId,
3103
+ roomId,
3104
+ content: {
3105
+ text: message,
3106
+ source: "discord",
3107
+ url: channel.url,
3108
+ name,
3109
+ userName,
3110
+ isVoiceMessage: true,
3111
+ channelType: type
3112
+ },
3113
+ createdAt: Date.now()
3114
+ };
3115
+ const callback = async (content, _files = []) => {
3116
+ try {
3117
+ const responseMemory = {
3118
+ id: createUniqueUuid5(
3119
+ this.runtime,
3120
+ `${memory.id}-voice-response-${Date.now()}`
3121
+ ),
3122
+ entityId: this.runtime.agentId,
3123
+ agentId: this.runtime.agentId,
3124
+ content: {
3125
+ ...content,
3126
+ name: this.runtime.character.name,
3127
+ inReplyTo: memory.id,
3128
+ isVoiceMessage: true,
3129
+ channelType: type
3130
+ },
3131
+ roomId,
3132
+ createdAt: Date.now()
3133
+ };
3134
+ if (responseMemory.content.text?.trim()) {
3135
+ await this.runtime.getMemoryManager("messages").createMemory(responseMemory);
3136
+ const responseStream = await this.runtime.useModel(
3137
+ ModelTypes9.TEXT_TO_SPEECH,
3138
+ content.text
3139
+ );
3140
+ if (responseStream) {
3141
+ await this.playAudioStream(entityId, responseStream);
3142
+ }
3143
+ }
3144
+ return [responseMemory];
3145
+ } catch (error) {
3146
+ console.error("Error in voice message callback:", error);
3147
+ return [];
3148
+ }
3149
+ };
3150
+ this.runtime.emitEvent(
3151
+ ["DISCORD_VOICE_MESSAGE_RECEIVED", "VOICE_MESSAGE_RECEIVED"],
3152
+ {
3153
+ runtime: this.runtime,
3154
+ message: memory,
3155
+ callback
3156
+ }
3157
+ );
3158
+ } catch (error) {
3159
+ console.error("Error processing voice message:", error);
3160
+ }
3161
+ }
3162
+ async convertOpusToWav(pcmBuffer) {
3163
+ try {
3164
+ const wavHeader = getWavHeader(pcmBuffer.length, DECODE_SAMPLE_RATE);
3165
+ const wavBuffer = Buffer.concat([wavHeader, pcmBuffer]);
3166
+ return wavBuffer;
3167
+ } catch (error) {
3168
+ console.error("Error converting PCM to WAV:", error);
3169
+ throw error;
3170
+ }
3171
+ }
3172
+ async scanGuild(guild) {
3173
+ let chosenChannel = null;
3174
+ try {
3175
+ const channelId = this.runtime.getSetting(
3176
+ "DISCORD_VOICE_CHANNEL_ID"
3177
+ );
3178
+ if (channelId) {
3179
+ const channel = await guild.channels.fetch(channelId);
3180
+ if (channel?.isVoiceBased()) {
3181
+ chosenChannel = channel;
3182
+ }
3183
+ }
3184
+ if (!chosenChannel) {
3185
+ const channels = (await guild.channels.fetch()).filter(
3186
+ (channel) => channel?.type === DiscordChannelType3.GuildVoice
3187
+ );
3188
+ for (const [, channel] of channels) {
3189
+ const voiceChannel = channel;
3190
+ if (voiceChannel.members.size > 0 && (chosenChannel === null || voiceChannel.members.size > chosenChannel.members.size)) {
3191
+ chosenChannel = voiceChannel;
3192
+ }
3193
+ }
3194
+ }
3195
+ if (chosenChannel) {
3196
+ console.log(`Joining channel: ${chosenChannel.name}`);
3197
+ await this.joinChannel(chosenChannel);
3198
+ } else {
3199
+ console.warn("No suitable voice channel found to join.");
3200
+ }
3201
+ } catch (error) {
3202
+ console.error("Error selecting or joining a voice channel:", error);
3203
+ }
3204
+ }
3205
+ async playAudioStream(entityId, audioStream) {
3206
+ const connection = this.connections.get(entityId);
3207
+ if (connection == null) {
3208
+ console.log(`No connection for user ${entityId}`);
3209
+ return;
3210
+ }
3211
+ this.cleanupAudioPlayer(this.activeAudioPlayer);
3212
+ const audioPlayer = createAudioPlayer2({
3213
+ behaviors: {
3214
+ noSubscriber: NoSubscriberBehavior2.Pause
3215
+ }
3216
+ });
3217
+ this.activeAudioPlayer = audioPlayer;
3218
+ connection.subscribe(audioPlayer);
3219
+ const audioStartTime = Date.now();
3220
+ const resource = createAudioResource2(audioStream, {
3221
+ inputType: StreamType.Arbitrary
3222
+ });
3223
+ audioPlayer.play(resource);
3224
+ audioPlayer.on("error", (err) => {
3225
+ console.log(`Audio player error: ${err}`);
3226
+ });
3227
+ audioPlayer.on(
3228
+ "stateChange",
3229
+ (_oldState, newState) => {
3230
+ if (newState.status === "idle") {
3231
+ const idleTime = Date.now();
3232
+ console.log(`Audio playback took: ${idleTime - audioStartTime}ms`);
3233
+ }
3234
+ }
3235
+ );
3236
+ }
3237
+ cleanupAudioPlayer(audioPlayer) {
3238
+ if (!audioPlayer) return;
3239
+ audioPlayer.stop();
3240
+ audioPlayer.removeAllListeners();
3241
+ if (audioPlayer === this.activeAudioPlayer) {
3242
+ this.activeAudioPlayer = null;
3243
+ }
3244
+ }
3245
+ async handleJoinChannelCommand(interaction) {
3246
+ try {
3247
+ await interaction.deferReply();
3248
+ const channelId = interaction.options.get("channel")?.value;
3249
+ if (!channelId) {
3250
+ await interaction.editReply("Please provide a voice channel to join.");
3251
+ return;
3252
+ }
3253
+ const guild = interaction.guild;
3254
+ if (!guild) {
3255
+ await interaction.editReply("Could not find guild.");
3256
+ return;
3257
+ }
3258
+ const voiceChannel = interaction.guild.channels.cache.find(
3259
+ (channel) => channel.id === channelId && channel.type === DiscordChannelType3.GuildVoice
3260
+ );
3261
+ if (!voiceChannel) {
3262
+ await interaction.editReply("Voice channel not found!");
3263
+ return;
3264
+ }
3265
+ await this.joinChannel(voiceChannel);
3266
+ await interaction.editReply(`Joined voice channel: ${voiceChannel.name}`);
3267
+ } catch (error) {
3268
+ console.error("Error joining voice channel:", error);
3269
+ await interaction.editReply("Failed to join the voice channel.").catch(console.error);
3270
+ }
3271
+ }
3272
+ async handleLeaveChannelCommand(interaction) {
3273
+ const connection = this.getVoiceConnection(interaction.guildId);
3274
+ if (!connection) {
3275
+ await interaction.reply("Not currently in a voice channel.");
3276
+ return;
3277
+ }
3278
+ try {
3279
+ connection.destroy();
3280
+ await interaction.reply("Left the voice channel.");
3281
+ } catch (error) {
3282
+ console.error("Error leaving voice channel:", error);
3283
+ await interaction.reply("Failed to leave the voice channel.");
3284
+ }
3285
+ }
3286
+ };
3287
+
3288
+ // src/index.ts
3289
+ var DiscordService = class _DiscordService extends Service {
3290
+ static serviceType = DISCORD_SERVICE_NAME;
3291
+ capabilityDescription = "The agent is able to send and receive messages on discord";
3292
+ client;
3293
+ character;
3294
+ messageManager;
3295
+ voiceManager;
3296
+ constructor(runtime) {
3297
+ super(runtime);
3298
+ logger7.log("Discord client constructor was engaged");
3299
+ this.client = new DiscordJsClient({
3300
+ intents: [
3301
+ GatewayIntentBits.Guilds,
3302
+ GatewayIntentBits.GuildMembers,
3303
+ GatewayIntentBits.GuildPresences,
3304
+ GatewayIntentBits.DirectMessages,
3305
+ GatewayIntentBits.GuildVoiceStates,
3306
+ GatewayIntentBits.MessageContent,
3307
+ GatewayIntentBits.GuildMessages,
3308
+ GatewayIntentBits.DirectMessageTyping,
3309
+ GatewayIntentBits.GuildMessageTyping,
3310
+ GatewayIntentBits.GuildMessageReactions
3311
+ ],
3312
+ partials: [
3313
+ Partials.Channel,
3314
+ Partials.Message,
3315
+ Partials.User,
3316
+ Partials.Reaction
3317
+ ]
3318
+ });
3319
+ this.runtime = runtime;
3320
+ this.voiceManager = new VoiceManager(this, runtime);
3321
+ this.messageManager = new MessageManager(this);
3322
+ this.client.once(Events2.ClientReady, this.onClientReady.bind(this));
3323
+ this.client.login(runtime.getSetting("DISCORD_API_TOKEN"));
3324
+ this.setupEventListeners();
3325
+ const ensureAllServersExist = async (runtime2) => {
3326
+ const guilds = await this.client.guilds.fetch();
3327
+ for (const [, guild] of guilds) {
3328
+ await this.ensureAllChannelsExist(runtime2, guild);
3329
+ }
3330
+ };
3331
+ ensureAllServersExist(this.runtime);
3332
+ }
3333
+ async ensureAllChannelsExist(runtime, guild) {
3334
+ const guildObj = await guild.fetch();
3335
+ const guildChannels = await guild.fetch();
3336
+ for (const [, channel] of guildChannels.channels.cache) {
3337
+ const roomId = createUniqueUuid6(this.runtime, channel.id);
3338
+ const room = await runtime.getDatabaseAdapter().getRoom(roomId);
3339
+ if (room) {
3340
+ continue;
3341
+ }
3342
+ const worldId = createUniqueUuid6(runtime, guild.id);
3343
+ const ownerId = createUniqueUuid6(this.runtime, guildObj.ownerId);
3344
+ await runtime.ensureWorldExists({
3345
+ id: worldId,
3346
+ name: guild.name,
3347
+ serverId: guild.id,
3348
+ agentId: runtime.agentId,
3349
+ metadata: {
3350
+ ownership: guildObj.ownerId ? { ownerId } : void 0,
3351
+ roles: {
3352
+ [ownerId]: Role.OWNER
3353
+ }
3354
+ }
3355
+ });
3356
+ await runtime.ensureRoomExists({
3357
+ id: roomId,
3358
+ name: channel.name,
3359
+ source: "discord",
3360
+ type: ChannelType10.GROUP,
3361
+ channelId: channel.id,
3362
+ serverId: guild.id,
3363
+ worldId
3364
+ });
3365
+ }
3366
+ }
3367
+ setupEventListeners() {
3368
+ this.client.on("guildCreate", this.handleGuildCreate.bind(this));
3369
+ this.client.on(
3370
+ Events2.MessageReactionAdd,
3371
+ this.handleReactionAdd.bind(this)
3372
+ );
3373
+ this.client.on(
3374
+ Events2.MessageReactionRemove,
3375
+ this.handleReactionRemove.bind(this)
3376
+ );
3377
+ this.client.on(Events2.GuildMemberAdd, this.handleGuildMemberAdd.bind(this));
3378
+ this.client.on(
3379
+ "voiceStateUpdate",
3380
+ this.voiceManager.handleVoiceStateUpdate.bind(this.voiceManager)
3381
+ );
3382
+ this.client.on(
3383
+ "userStream",
3384
+ this.voiceManager.handleUserStream.bind(this.voiceManager)
3385
+ );
3386
+ this.client.on(
3387
+ Events2.MessageCreate,
3388
+ this.messageManager.handleMessage.bind(this.messageManager)
3389
+ );
3390
+ this.client.on(
3391
+ Events2.InteractionCreate,
3392
+ this.handleInteractionCreate.bind(this)
3393
+ );
3394
+ }
3395
+ async handleGuildMemberAdd(member) {
3396
+ logger7.log(`New member joined: ${member.user.username}`);
3397
+ const guild = member.guild;
3398
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3399
+ this.runtime.emitEvent("USER_JOINED", {
3400
+ runtime: this.runtime,
3401
+ entityId: createUniqueUuid6(this.runtime, member.id),
3402
+ user: {
3403
+ id: member.id,
3404
+ username: tag,
3405
+ displayName: member.displayName || member.user.username
3406
+ },
3407
+ serverId: guild.id,
3408
+ channelId: null,
3409
+ // No specific channel for server joins
3410
+ channelType: ChannelType10.WORLD,
3411
+ source: "discord"
3412
+ });
3413
+ this.runtime.emitEvent("DISCORD_USER_JOINED", {
3414
+ runtime: this.runtime,
3415
+ entityId: createUniqueUuid6(this.runtime, member.id),
3416
+ member,
3417
+ guild
3418
+ });
3419
+ }
3420
+ static async start(runtime) {
3421
+ const client = new _DiscordService(runtime);
3422
+ return client;
3423
+ }
3424
+ static async stop(runtime) {
3425
+ const client = runtime.getService(DISCORD_SERVICE_NAME);
3426
+ if (!client) {
3427
+ logger7.error("DiscordService not found");
3428
+ return;
3429
+ }
3430
+ try {
3431
+ await client.stop();
3432
+ } catch (e) {
3433
+ logger7.error("client-discord instance stop err", e);
3434
+ }
3435
+ }
3436
+ async stop() {
3437
+ await this.client.destroy();
3438
+ }
3439
+ async onClientReady(readyClient) {
3440
+ logger7.success(`Logged in as ${readyClient.user?.tag}`);
3441
+ const commands = [
3442
+ {
3443
+ name: "joinchannel",
3444
+ description: "Join a voice channel",
3445
+ options: [
3446
+ {
3447
+ name: "channel",
3448
+ type: 7,
3449
+ // CHANNEL type
3450
+ description: "The voice channel to join",
3451
+ required: true,
3452
+ channel_types: [2]
3453
+ // GuildVoice type
3454
+ }
3455
+ ]
3456
+ },
3457
+ {
3458
+ name: "leavechannel",
3459
+ description: "Leave the current voice channel"
3460
+ }
3461
+ ];
3462
+ try {
3463
+ await this.client.application?.commands.set(commands);
3464
+ logger7.success("Slash commands registered");
3465
+ } catch (error) {
3466
+ console.error("Error registering slash commands:", error);
3467
+ }
3468
+ const requiredPermissions = [
3469
+ // Text Permissions
3470
+ PermissionsBitField2.Flags.ViewChannel,
3471
+ PermissionsBitField2.Flags.SendMessages,
3472
+ PermissionsBitField2.Flags.SendMessagesInThreads,
3473
+ PermissionsBitField2.Flags.CreatePrivateThreads,
3474
+ PermissionsBitField2.Flags.CreatePublicThreads,
3475
+ PermissionsBitField2.Flags.EmbedLinks,
3476
+ PermissionsBitField2.Flags.AttachFiles,
3477
+ PermissionsBitField2.Flags.AddReactions,
3478
+ PermissionsBitField2.Flags.UseExternalEmojis,
3479
+ PermissionsBitField2.Flags.UseExternalStickers,
3480
+ PermissionsBitField2.Flags.MentionEveryone,
3481
+ PermissionsBitField2.Flags.ManageMessages,
3482
+ PermissionsBitField2.Flags.ReadMessageHistory,
3483
+ // Voice Permissions
3484
+ PermissionsBitField2.Flags.Connect,
3485
+ PermissionsBitField2.Flags.Speak,
3486
+ PermissionsBitField2.Flags.UseVAD,
3487
+ PermissionsBitField2.Flags.PrioritySpeaker
3488
+ ].reduce((a, b) => a | b, 0n);
3489
+ logger7.success("Use this URL to add the bot to your server:");
3490
+ logger7.success(
3491
+ `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user?.id}&permissions=${requiredPermissions}&scope=bot%20applications.commands`
3492
+ );
3493
+ await this.onReady();
3494
+ }
3495
+ async getChannelType(channel) {
3496
+ switch (channel.type) {
3497
+ case DiscordChannelType4.DM:
3498
+ return ChannelType10.DM;
3499
+ case DiscordChannelType4.GuildText:
3500
+ return ChannelType10.GROUP;
3501
+ case DiscordChannelType4.GuildVoice:
3502
+ return ChannelType10.VOICE_GROUP;
3503
+ }
3504
+ }
3505
+ async handleReactionAdd(reaction, user) {
3506
+ try {
3507
+ logger7.log("Reaction added");
3508
+ if (!reaction || !user) {
3509
+ logger7.warn("Invalid reaction or user");
3510
+ return;
3511
+ }
3512
+ let emoji = reaction.emoji.name;
3513
+ if (!emoji && reaction.emoji.id) {
3514
+ emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`;
3515
+ }
3516
+ if (reaction.partial) {
3517
+ try {
3518
+ await reaction.fetch();
3519
+ } catch (error) {
3520
+ logger7.error("Failed to fetch partial reaction:", error);
3521
+ return;
3522
+ }
3523
+ }
3524
+ const timestamp = Date.now();
3525
+ const roomId = createUniqueUuid6(
3526
+ this.runtime,
3527
+ reaction.message.channel.id
3528
+ );
3529
+ const entityId = createUniqueUuid6(this.runtime, user.id);
3530
+ const reactionUUID = createUniqueUuid6(
3531
+ this.runtime,
3532
+ `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`
3533
+ );
3534
+ if (!entityId || !roomId) {
3535
+ logger7.error("Invalid user ID or room ID", {
3536
+ entityId,
3537
+ roomId
3538
+ });
3539
+ return;
3540
+ }
3541
+ const messageContent = reaction.message.content || "";
3542
+ const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3543
+ const reactionMessage = `*Added <${emoji}> to: "${truncatedContent}"*`;
3544
+ const userName = reaction.message.author?.username || "unknown";
3545
+ const name = reaction.message.author?.displayName || userName;
3546
+ await this.runtime.ensureConnection({
3547
+ entityId,
3548
+ roomId,
3549
+ userName,
3550
+ name,
3551
+ source: "discord",
3552
+ channelId: reaction.message.channel.id,
3553
+ serverId: reaction.message.guild?.id,
3554
+ type: await this.getChannelType(reaction.message.channel)
3555
+ });
3556
+ const inReplyTo = createUniqueUuid6(this.runtime, reaction.message.id);
3557
+ const memory = {
3558
+ id: reactionUUID,
3559
+ entityId,
3560
+ agentId: this.runtime.agentId,
3561
+ content: {
3562
+ // name,
3563
+ // userName,
3564
+ text: reactionMessage,
3565
+ source: "discord",
3566
+ inReplyTo,
3567
+ channelType: await this.getChannelType(
3568
+ reaction.message.channel
3569
+ )
3570
+ },
3571
+ roomId,
3572
+ createdAt: timestamp
3573
+ };
3574
+ const callback = async (content) => {
3575
+ if (!reaction.message.channel) {
3576
+ logger7.error("No channel found for reaction message");
3577
+ return;
3578
+ }
3579
+ await reaction.message.channel.send(content.text);
3580
+ return [];
3581
+ };
3582
+ this.runtime.emitEvent(
3583
+ ["DISCORD_REACTION_RECEIVED", "REACTION_RECEIVED"],
3584
+ {
3585
+ runtime: this.runtime,
3586
+ message: memory,
3587
+ callback
3588
+ }
3589
+ );
3590
+ } catch (error) {
3591
+ logger7.error("Error handling reaction:", error);
3592
+ }
3593
+ }
3594
+ async handleReactionRemove(reaction, user) {
3595
+ try {
3596
+ logger7.log("Reaction removed");
3597
+ let emoji = reaction.emoji.name;
3598
+ if (!emoji && reaction.emoji.id) {
3599
+ emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`;
3600
+ }
3601
+ if (reaction.partial) {
3602
+ try {
3603
+ await reaction.fetch();
3604
+ } catch (error) {
3605
+ logger7.error(
3606
+ "Something went wrong when fetching the message:",
3607
+ error
3608
+ );
3609
+ return;
3610
+ }
3611
+ }
3612
+ const messageContent = reaction.message.content || "";
3613
+ const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3614
+ const reactionMessage = `*Removed <${emoji}> from: "${truncatedContent}"*`;
3615
+ const roomId = createUniqueUuid6(
3616
+ this.runtime,
3617
+ reaction.message.channel.id
3618
+ );
3619
+ const entityId = createUniqueUuid6(this.runtime, user.id);
3620
+ const timestamp = Date.now();
3621
+ const reactionUUID = createUniqueUuid6(
3622
+ this.runtime,
3623
+ `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`
3624
+ );
3625
+ const userName = reaction.message.author?.username || "unknown";
3626
+ const name = reaction.message.author?.displayName || userName;
3627
+ await this.runtime.ensureConnection({
3628
+ entityId,
3629
+ roomId,
3630
+ userName,
3631
+ name,
3632
+ source: "discord",
3633
+ channelId: reaction.message.channel.id,
3634
+ serverId: reaction.message.guild?.id,
3635
+ type: await this.getChannelType(reaction.message.channel)
3636
+ });
3637
+ const memory = {
3638
+ id: reactionUUID,
3639
+ entityId,
3640
+ agentId: this.runtime.agentId,
3641
+ content: {
3642
+ // name,
3643
+ // userName,
3644
+ text: reactionMessage,
3645
+ source: "discord",
3646
+ inReplyTo: createUniqueUuid6(this.runtime, reaction.message.id),
3647
+ channelType: await this.getChannelType(
3648
+ reaction.message.channel
3649
+ )
3650
+ },
3651
+ roomId,
3652
+ createdAt: Date.now()
3653
+ };
3654
+ const callback = async (content) => {
3655
+ if (!reaction.message.channel) {
3656
+ logger7.error("No channel found for reaction message");
3657
+ return;
3658
+ }
3659
+ await reaction.message.channel.send(content.text);
3660
+ return [];
3661
+ };
3662
+ this.runtime.emitEvent(["DISCORD_REACTION_EVENT", "REACTION_RECEIVED"], {
3663
+ runtime: this.runtime,
3664
+ message: memory,
3665
+ callback
3666
+ });
3667
+ } catch (error) {
3668
+ logger7.error("Error handling reaction removal:", error);
3669
+ }
3670
+ }
3671
+ async handleGuildCreate(guild) {
3672
+ logger7.log(`Joined guild ${guild.name}`);
3673
+ const fullGuild = await guild.fetch();
3674
+ this.voiceManager.scanGuild(guild);
3675
+ const ownerId = createUniqueUuid6(this.runtime, fullGuild.ownerId);
3676
+ const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
3677
+ const standardizedData = {
3678
+ runtime: this.runtime,
3679
+ rooms: await this.buildStandardizedRooms(fullGuild, worldId),
3680
+ users: await this.buildStandardizedUsers(fullGuild),
3681
+ world: {
3682
+ id: worldId,
3683
+ name: fullGuild.name,
3684
+ agentId: this.runtime.agentId,
3685
+ serverId: fullGuild.id,
3686
+ metadata: {
3687
+ ownership: fullGuild.ownerId ? { ownerId } : void 0,
3688
+ roles: {
3689
+ [ownerId]: Role.OWNER
3690
+ }
3691
+ }
3692
+ },
3693
+ source: "discord"
3694
+ };
3695
+ this.runtime.emitEvent(["DISCORD_SERVER_JOINED"], {
3696
+ runtime: this.runtime,
3697
+ server: fullGuild,
3698
+ source: "discord"
3699
+ });
3700
+ this.runtime.emitEvent(["SERVER_JOINED"], standardizedData);
3701
+ }
3702
+ async handleInteractionCreate(interaction) {
3703
+ if (!interaction.isCommand()) return;
3704
+ switch (interaction.commandName) {
3705
+ case "joinchannel":
3706
+ await this.voiceManager.handleJoinChannelCommand(interaction);
3707
+ break;
3708
+ case "leavechannel":
3709
+ await this.voiceManager.handleLeaveChannelCommand(interaction);
3710
+ break;
3711
+ }
3712
+ }
3713
+ /**
3714
+ * Builds a standardized list of rooms from Discord guild channels
3715
+ */
3716
+ async buildStandardizedRooms(guild, _worldId) {
3717
+ const rooms = [];
3718
+ for (const [channelId, channel] of guild.channels.cache) {
3719
+ if (channel.type === DiscordChannelType4.GuildText || channel.type === DiscordChannelType4.GuildVoice) {
3720
+ const roomId = createUniqueUuid6(this.runtime, channelId);
3721
+ let channelType;
3722
+ switch (channel.type) {
3723
+ case DiscordChannelType4.GuildText:
3724
+ channelType = ChannelType10.GROUP;
3725
+ break;
3726
+ case DiscordChannelType4.GuildVoice:
3727
+ channelType = ChannelType10.VOICE_GROUP;
3728
+ break;
3729
+ default:
3730
+ channelType = ChannelType10.GROUP;
3731
+ }
3732
+ let participants = [];
3733
+ if (guild.memberCount < 1e3 && channel.type === DiscordChannelType4.GuildText) {
3734
+ try {
3735
+ participants = Array.from(guild.members.cache.values()).filter(
3736
+ (member) => channel.permissionsFor(member)?.has(PermissionsBitField2.Flags.ViewChannel)
3737
+ ).map((member) => createUniqueUuid6(this.runtime, member.id));
3738
+ } catch (error) {
3739
+ logger7.warn(
3740
+ `Failed to get participants for channel ${channel.name}:`,
3741
+ error
3742
+ );
3743
+ }
3744
+ }
3745
+ rooms.push({
3746
+ id: roomId,
3747
+ name: channel.name,
3748
+ type: channelType,
3749
+ channelId: channel.id,
3750
+ participants
3751
+ });
3752
+ }
3753
+ }
3754
+ return rooms;
3755
+ }
3756
+ /**
3757
+ * Builds a standardized list of users from Discord guild members
3758
+ */
3759
+ async buildStandardizedUsers(guild) {
3760
+ const entities = [];
3761
+ const botId = this.client.user?.id;
3762
+ if (guild.memberCount > 1e3) {
3763
+ logger7.info(
3764
+ `Using optimized user sync for large guild ${guild.name} (${guild.memberCount} members)`
3765
+ );
3766
+ try {
3767
+ for (const [, member] of guild.members.cache) {
3768
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3769
+ if (member.id !== botId) {
3770
+ entities.push({
3771
+ id: createUniqueUuid6(this.runtime, member.id),
3772
+ names: Array.from(
3773
+ /* @__PURE__ */ new Set([
3774
+ member.user.username,
3775
+ member.displayName,
3776
+ member.user.globalName
3777
+ ])
3778
+ ),
3779
+ agentId: this.runtime.agentId,
3780
+ metadata: {
3781
+ default: {
3782
+ username: tag,
3783
+ name: member.displayName || member.user.username
3784
+ },
3785
+ discord: member.user.globalName ? {
3786
+ username: tag,
3787
+ name: member.displayName || member.user.username,
3788
+ globalName: member.user.globalName,
3789
+ userId: member.id
3790
+ } : {
3791
+ username: tag,
3792
+ name: member.displayName || member.user.username,
3793
+ userId: member.id
3794
+ }
3795
+ }
3796
+ });
3797
+ }
3798
+ }
3799
+ if (entities.length < 100) {
3800
+ logger7.info(`Adding online members for ${guild.name}`);
3801
+ const onlineMembers = await guild.members.fetch({ limit: 100 });
3802
+ for (const [, member] of onlineMembers) {
3803
+ if (member.id !== botId) {
3804
+ const entityId = createUniqueUuid6(this.runtime, member.id);
3805
+ if (!entities.some((u) => u.id === entityId)) {
3806
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3807
+ entities.push({
3808
+ id: entityId,
3809
+ names: Array.from(
3810
+ /* @__PURE__ */ new Set([
3811
+ member.user.username,
3812
+ member.displayName,
3813
+ member.user.globalName
3814
+ ])
3815
+ ),
3816
+ agentId: this.runtime.agentId,
3817
+ metadata: {
3818
+ default: {
3819
+ username: tag,
3820
+ name: member.displayName || member.user.username
3821
+ },
3822
+ discord: member.user.globalName ? {
3823
+ username: tag,
3824
+ name: member.displayName || member.user.username,
3825
+ globalName: member.user.globalName,
3826
+ userId: member.id
3827
+ } : {
3828
+ username: tag,
3829
+ name: member.displayName || member.user.username,
3830
+ userId: member.id
3831
+ }
3832
+ }
3833
+ });
3834
+ }
3835
+ }
3836
+ }
3837
+ }
3838
+ } catch (error) {
3839
+ logger7.error(`Error fetching members for ${guild.name}:`, error);
3840
+ }
3841
+ } else {
3842
+ try {
3843
+ let members = guild.members.cache;
3844
+ if (members.size === 0) {
3845
+ members = await guild.members.fetch();
3846
+ }
3847
+ for (const [, member] of members) {
3848
+ if (member.id !== botId) {
3849
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3850
+ entities.push({
3851
+ id: createUniqueUuid6(this.runtime, member.id),
3852
+ names: Array.from(
3853
+ /* @__PURE__ */ new Set([
3854
+ member.user.username,
3855
+ member.displayName,
3856
+ member.user.globalName
3857
+ ])
3858
+ ),
3859
+ agentId: this.runtime.agentId,
3860
+ metadata: {
3861
+ default: {
3862
+ username: tag,
3863
+ name: member.displayName || member.user.username
3864
+ },
3865
+ discord: member.user.globalName ? {
3866
+ username: tag,
3867
+ name: member.displayName || member.user.username,
3868
+ globalName: member.user.globalName,
3869
+ userId: member.id
3870
+ } : {
3871
+ username: tag,
3872
+ name: member.displayName || member.user.username,
3873
+ userId: member.id
3874
+ }
3875
+ }
3876
+ });
3877
+ }
3878
+ }
3879
+ } catch (error) {
3880
+ logger7.error(`Error fetching members for ${guild.name}:`, error);
3881
+ }
3882
+ }
3883
+ return entities;
3884
+ }
3885
+ async onReady() {
3886
+ logger7.log("DISCORD ON READY");
3887
+ const guilds = await this.client.guilds.fetch();
3888
+ for (const [, guild] of guilds) {
3889
+ const fullGuild = await guild.fetch();
3890
+ await this.voiceManager.scanGuild(fullGuild);
3891
+ setTimeout(async () => {
3892
+ const fullGuild2 = await guild.fetch();
3893
+ logger7.log("DISCORD SERVER CONNECTED", fullGuild2.name);
3894
+ this.runtime.emitEvent(["DISCORD_SERVER_CONNECTED"], {
3895
+ runtime: this.runtime,
3896
+ server: fullGuild2,
3897
+ source: "discord"
3898
+ });
3899
+ const worldId = createUniqueUuid6(this.runtime, fullGuild2.id);
3900
+ const ownerId = createUniqueUuid6(this.runtime, fullGuild2.ownerId);
3901
+ const standardizedData = {
3902
+ name: fullGuild2.name,
3903
+ runtime: this.runtime,
3904
+ rooms: await this.buildStandardizedRooms(fullGuild2, worldId),
3905
+ users: await this.buildStandardizedUsers(fullGuild2),
3906
+ world: {
3907
+ id: worldId,
3908
+ name: fullGuild2.name,
3909
+ agentId: this.runtime.agentId,
3910
+ serverId: fullGuild2.id,
3911
+ metadata: {
3912
+ ownership: fullGuild2.ownerId ? { ownerId } : void 0,
3913
+ roles: {
3914
+ [ownerId]: Role.OWNER
3915
+ }
3916
+ }
3917
+ },
3918
+ source: "discord"
3919
+ };
3920
+ this.runtime.emitEvent(["SERVER_CONNECTED"], standardizedData);
3921
+ }, 1e3);
3922
+ }
3923
+ this.client.emit("voiceManagerReady");
3924
+ }
3925
+ };
3926
+ var discordPlugin = {
3927
+ name: "discord",
3928
+ description: "Discord client plugin",
3929
+ services: [DiscordService],
3930
+ actions: [
3931
+ chatWithAttachments_default,
3932
+ downloadMedia_default,
3933
+ voiceJoin_default,
3934
+ voiceLeave_default,
3935
+ summarizeConversation_default,
3936
+ transcribeMedia_default
3937
+ ],
3938
+ providers: [channelState_default, voiceState_default],
3939
+ tests: [new DiscordTestSuite()]
3940
+ };
3941
+ var index_default = discordPlugin;
3942
+ export {
3943
+ DiscordService,
3944
+ index_default as default
3945
+ };
3946
+ //# sourceMappingURL=index.js.map