@elizaos/plugin-discord 1.0.0-alpha.4 → 1.0.0-alpha.41

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 CHANGED
@@ -1,29 +1,17 @@
1
1
  // src/index.ts
2
2
  import {
3
- ChannelType as ChannelType10,
4
- createUniqueUuid as createUniqueUuid6,
5
- logger as logger7,
6
- Role,
7
- Service
3
+ logger as logger8
8
4
  } 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
5
 
18
6
  // src/actions/chatWithAttachments.ts
7
+ import fs from "node:fs";
19
8
  import {
20
9
  ChannelType,
21
- composePrompt,
22
10
  ModelTypes,
11
+ composePromptFromState,
23
12
  parseJSONObjectFromText,
24
13
  trimTokens
25
14
  } from "@elizaos/core";
26
- import * as fs from "node:fs";
27
15
  var summarizationTemplate = `# Summarized so far (we are adding to this)
28
16
  {{currentSummary}}
29
17
 
@@ -49,7 +37,7 @@ Your response must be formatted as a JSON block with this structure:
49
37
  \`\`\`
50
38
  `;
51
39
  var getAttachmentIds = async (runtime, _message, state) => {
52
- const prompt = composePrompt({
40
+ const prompt = composePromptFromState({
53
41
  state,
54
42
  template: attachmentIdsTemplate
55
43
  });
@@ -65,7 +53,7 @@ var getAttachmentIds = async (runtime, _message, state) => {
65
53
  }
66
54
  return null;
67
55
  };
68
- var summarizeAction = {
56
+ var chatWithAttachments = {
69
57
  name: "CHAT_WITH_ATTACHMENTS",
70
58
  similes: [
71
59
  "CHAT_WITH_ATTACHMENT",
@@ -86,7 +74,7 @@ var summarizeAction = {
86
74
  ],
87
75
  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
76
  validate: async (_runtime, message, _state) => {
89
- const room = await _runtime.getDatabaseAdapter().getRoom(message.roomId);
77
+ const room = await _runtime.getRoom(message.roomId);
90
78
  if (room?.type !== ChannelType.GROUP) {
91
79
  return false;
92
80
  }
@@ -166,7 +154,7 @@ ${attachment.text}`).join("\n\n");
166
154
  chunkSize,
167
155
  runtime
168
156
  );
169
- const prompt = composePrompt({
157
+ const prompt = composePromptFromState({
170
158
  state,
171
159
  // make sure it fits, we can pad the tokens a bit
172
160
  // Get the model's tokenizer based on the current model being used
@@ -213,7 +201,7 @@ ${currentSummary.trim()}
213
201
  });
214
202
  await fs.promises.writeFile(summaryFilename, currentSummary, "utf8");
215
203
  console.log("File written successfully");
216
- await runtime.getDatabaseAdapter().setCache(summaryFilename, currentSummary);
204
+ await runtime.setCache(summaryFilename, currentSummary);
217
205
  console.log("Cache set operation completed");
218
206
  await callback(
219
207
  {
@@ -297,14 +285,14 @@ ${currentSummary.trim()}
297
285
  ]
298
286
  ]
299
287
  };
300
- var chatWithAttachments_default = summarizeAction;
288
+ var chatWithAttachments_default = chatWithAttachments;
301
289
 
302
290
  // src/actions/downloadMedia.ts
303
291
  import {
304
- composePrompt as composePrompt2,
305
292
  ModelTypes as ModelTypes2,
306
- parseJSONObjectFromText as parseJSONObjectFromText2,
307
- ServiceTypes
293
+ ServiceTypes,
294
+ composePromptFromState as composePromptFromState2,
295
+ parseJSONObjectFromText as parseJSONObjectFromText2
308
296
  } from "@elizaos/core";
309
297
  var mediaUrlTemplate = `# Messages we are searching for a media URL
310
298
  {{recentMessages}}
@@ -320,7 +308,7 @@ Your response must be formatted as a JSON block with this structure:
320
308
  \`\`\`
321
309
  `;
322
310
  var getMediaUrl = async (runtime, _message, state) => {
323
- const prompt = composePrompt2({
311
+ const prompt = composePromptFromState2({
324
312
  state,
325
313
  template: mediaUrlTemplate
326
314
  });
@@ -335,7 +323,7 @@ var getMediaUrl = async (runtime, _message, state) => {
335
323
  }
336
324
  return null;
337
325
  };
338
- var downloadMedia_default = {
326
+ var downloadMedia = {
339
327
  name: "DOWNLOAD_MEDIA",
340
328
  similes: [
341
329
  "DOWNLOAD_VIDEO",
@@ -453,15 +441,15 @@ var downloadMedia_default = {
453
441
  };
454
442
 
455
443
  // src/actions/summarizeConversation.ts
444
+ import fs2 from "node:fs";
456
445
  import {
457
- composePrompt as composePrompt3,
458
- getEntityDetails,
459
446
  ModelTypes as ModelTypes3,
447
+ composePromptFromState as composePromptFromState3,
448
+ getEntityDetails,
460
449
  parseJSONObjectFromText as parseJSONObjectFromText3,
461
450
  splitChunks,
462
451
  trimTokens as trimTokens2
463
452
  } from "@elizaos/core";
464
- import * as fs2 from "node:fs";
465
453
  var summarizationTemplate2 = `# Summarized so far (we are adding to this)
466
454
  {{currentSummary}}
467
455
 
@@ -490,7 +478,7 @@ Your response must be formatted as a JSON block with this structure:
490
478
  \`\`\`
491
479
  `;
492
480
  var getDateRange = async (runtime, _message, state) => {
493
- const prompt = composePrompt3({
481
+ const prompt = composePromptFromState3({
494
482
  state,
495
483
  template: dateRangeTemplate
496
484
  });
@@ -533,7 +521,7 @@ var getDateRange = async (runtime, _message, state) => {
533
521
  }
534
522
  }
535
523
  };
536
- var summarizeAction2 = {
524
+ var summarize = {
537
525
  name: "SUMMARIZE_CONVERSATION",
538
526
  similes: [
539
527
  "RECAP",
@@ -656,7 +644,7 @@ ${attachments}`;
656
644
  chunkSize + 500,
657
645
  runtime
658
646
  );
659
- const prompt = composePrompt3({
647
+ const prompt = composePromptFromState3({
660
648
  state,
661
649
  // make sure it fits, we can pad the tokens a bit
662
650
  template
@@ -695,7 +683,7 @@ ${currentSummary.trim()}
695
683
  } else if (currentSummary.trim()) {
696
684
  const summaryDir = "cache";
697
685
  const summaryFilename = `${summaryDir}/conversation_summary_${Date.now()}`;
698
- await runtime.getDatabaseAdapter().setCache(summaryFilename, currentSummary);
686
+ await runtime.setCache(summaryFilename, currentSummary);
699
687
  await fs2.promises.mkdir(summaryDir, { recursive: true });
700
688
  await fs2.promises.writeFile(summaryFilename, currentSummary, "utf8");
701
689
  await callback(
@@ -781,12 +769,11 @@ ${currentSummary.trim()}
781
769
  ]
782
770
  ]
783
771
  };
784
- var summarizeConversation_default = summarizeAction2;
785
772
 
786
773
  // src/actions/transcribeMedia.ts
787
774
  import {
788
- composePrompt as composePrompt4,
789
775
  ModelTypes as ModelTypes4,
776
+ composePromptFromState as composePromptFromState4,
790
777
  parseJSONObjectFromText as parseJSONObjectFromText4
791
778
  } from "@elizaos/core";
792
779
  var mediaAttachmentIdTemplate = `# Messages we are transcribing
@@ -803,7 +790,7 @@ Your response must be formatted as a JSON block with this structure:
803
790
  \`\`\`
804
791
  `;
805
792
  var getMediaAttachmentId = async (runtime, _message, state) => {
806
- const prompt = composePrompt4({
793
+ const prompt = composePromptFromState4({
807
794
  state,
808
795
  template: mediaAttachmentIdTemplate
809
796
  });
@@ -819,7 +806,7 @@ var getMediaAttachmentId = async (runtime, _message, state) => {
819
806
  }
820
807
  return null;
821
808
  };
822
- var transcribeMediaAction = {
809
+ var transcribeMedia = {
823
810
  name: "TRANSCRIBE_MEDIA",
824
811
  similes: [
825
812
  "TRANSCRIBE_AUDIO",
@@ -913,7 +900,7 @@ ${mediaTranscript.trim()}
913
900
  await callback(callbackData);
914
901
  } else if (callbackData.text) {
915
902
  const transcriptFilename = `content/transcript_${Date.now()}`;
916
- await runtime.getDatabaseAdapter().setCache(transcriptFilename, callbackData.text);
903
+ await runtime.setCache(transcriptFilename, callbackData.text);
917
904
  await callback(
918
905
  {
919
906
  ...callbackData,
@@ -959,15 +946,14 @@ ${mediaTranscript.trim()}
959
946
  ]
960
947
  ]
961
948
  };
962
- var transcribeMedia_default = transcribeMediaAction;
963
949
 
964
950
  // src/actions/voiceJoin.ts
965
951
  import {
966
952
  ChannelType as ChannelType2,
967
- composePrompt as composePrompt5,
953
+ ModelTypes as ModelTypes5,
954
+ composePromptFromState as composePromptFromState5,
968
955
  createUniqueUuid as createUniqueUuid2,
969
- logger,
970
- ModelTypes as ModelTypes5
956
+ logger
971
957
  } from "@elizaos/core";
972
958
  import {
973
959
  ChannelType as DiscordChannelType
@@ -979,7 +965,7 @@ var ServiceTypes2 = {
979
965
  };
980
966
 
981
967
  // src/actions/voiceJoin.ts
982
- var voiceJoin_default = {
968
+ var joinVoice = {
983
969
  name: "JOIN_VOICE",
984
970
  similes: [
985
971
  "JOIN_VOICE",
@@ -993,7 +979,7 @@ var voiceJoin_default = {
993
979
  if (message.content.source !== "discord") {
994
980
  return false;
995
981
  }
996
- const room = state.data.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
982
+ const room = state.data.room ?? await runtime.getRoom(message.roomId);
997
983
  if (room?.type !== ChannelType2.GROUP) {
998
984
  return false;
999
985
  }
@@ -1006,7 +992,7 @@ var voiceJoin_default = {
1006
992
  },
1007
993
  description: "Join a voice channel to participate in voice chat.",
1008
994
  handler: async (runtime, message, state, _options, callback) => {
1009
- const room = state.data.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
995
+ const room = state.data.room ?? await runtime.getRoom(message.roomId);
1010
996
  if (!room) {
1011
997
  throw new Error("No room found");
1012
998
  }
@@ -1090,7 +1076,7 @@ You should only respond with the name of the voice channel or none, no commentar
1090
1076
  userMessage: message.content.text,
1091
1077
  voiceChannels: voiceChannels.map((channel) => channel.name).join("\n")
1092
1078
  };
1093
- const prompt = composePrompt5({
1079
+ const prompt = composePromptFromState5({
1094
1080
  template: messageTemplate,
1095
1081
  state: guessState
1096
1082
  });
@@ -1272,7 +1258,7 @@ import {
1272
1258
  logger as logger2
1273
1259
  } from "@elizaos/core";
1274
1260
  import { BaseGuildVoiceChannel } from "discord.js";
1275
- var voiceLeave_default = {
1261
+ var leaveVoice = {
1276
1262
  name: "LEAVE_VOICE",
1277
1263
  similes: [
1278
1264
  "LEAVE_VOICE",
@@ -1291,7 +1277,7 @@ var voiceLeave_default = {
1291
1277
  logger2.error("Discord client not found");
1292
1278
  return false;
1293
1279
  }
1294
- const room = state.data.room ?? await runtime.getDatabaseAdapter().getRoom(message.roomId);
1280
+ const room = state.data.room ?? await runtime.getRoom(message.roomId);
1295
1281
  if (room?.type !== ChannelType3.GROUP) {
1296
1282
  return false;
1297
1283
  }
@@ -1300,7 +1286,7 @@ var voiceLeave_default = {
1300
1286
  },
1301
1287
  description: "Leave the current voice channel.",
1302
1288
  handler: async (runtime, message, _state, _options) => {
1303
- const room = await runtime.getDatabaseAdapter().getRoom(message.roomId);
1289
+ const room = await runtime.getRoom(message.roomId);
1304
1290
  if (!room) {
1305
1291
  throw new Error("No room found");
1306
1292
  }
@@ -1519,6 +1505,187 @@ var voiceLeave_default = {
1519
1505
  ]
1520
1506
  };
1521
1507
 
1508
+ // src/providers/channelState.ts
1509
+ import { ChannelType as ChannelType4 } from "@elizaos/core";
1510
+ var channelStateProvider = {
1511
+ name: "channelState",
1512
+ get: async (runtime, message, state) => {
1513
+ const room = state.data?.room ?? await runtime.getRoom(message.roomId);
1514
+ if (!room) {
1515
+ throw new Error("No room found");
1516
+ }
1517
+ if (message.content.source !== "discord") {
1518
+ return {
1519
+ data: null,
1520
+ values: {},
1521
+ text: ""
1522
+ };
1523
+ }
1524
+ const agentName = state?.agentName || "The agent";
1525
+ const senderName = state?.senderName || "someone";
1526
+ let responseText = "";
1527
+ let channelType = "";
1528
+ let serverName = "";
1529
+ let channelId = "";
1530
+ const serverId = room.serverId;
1531
+ if (room.type === ChannelType4.DM) {
1532
+ channelType = "DM";
1533
+ 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.`;
1534
+ } else {
1535
+ channelType = "GROUP";
1536
+ if (!serverId) {
1537
+ console.error("No server ID found");
1538
+ return {
1539
+ data: {
1540
+ room,
1541
+ channelType
1542
+ },
1543
+ values: {
1544
+ channelType
1545
+ },
1546
+ text: ""
1547
+ };
1548
+ }
1549
+ channelId = room.channelId;
1550
+ const discordService = runtime.getService(
1551
+ ServiceTypes2.DISCORD
1552
+ );
1553
+ if (!discordService) {
1554
+ console.warn("No discord client found");
1555
+ return {
1556
+ data: {
1557
+ room,
1558
+ channelType,
1559
+ serverId
1560
+ },
1561
+ values: {
1562
+ channelType,
1563
+ serverId
1564
+ },
1565
+ text: ""
1566
+ };
1567
+ }
1568
+ const guild = discordService.client.guilds.cache.get(serverId);
1569
+ serverName = guild.name;
1570
+ responseText = `${agentName} is currently having a conversation in the channel \`@${channelId} in the server \`${serverName}\` (@${serverId})`;
1571
+ responseText += `
1572
+ ${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.`;
1573
+ }
1574
+ return {
1575
+ data: {
1576
+ room,
1577
+ channelType,
1578
+ serverId,
1579
+ serverName,
1580
+ channelId
1581
+ },
1582
+ values: {
1583
+ channelType,
1584
+ serverName,
1585
+ channelId
1586
+ },
1587
+ text: responseText
1588
+ };
1589
+ }
1590
+ };
1591
+
1592
+ // src/providers/voiceState.ts
1593
+ import { getVoiceConnection } from "@discordjs/voice";
1594
+ import { ChannelType as ChannelType5 } from "@elizaos/core";
1595
+ var voiceStateProvider = {
1596
+ name: "voiceState",
1597
+ get: async (runtime, message, state) => {
1598
+ const room = await runtime.getRoom(message.roomId);
1599
+ if (!room) {
1600
+ throw new Error("No room found");
1601
+ }
1602
+ if (room.type !== ChannelType5.GROUP) {
1603
+ return {
1604
+ data: {
1605
+ isInVoiceChannel: false,
1606
+ room
1607
+ },
1608
+ values: {
1609
+ isInVoiceChannel: "false",
1610
+ roomType: room.type
1611
+ },
1612
+ text: ""
1613
+ };
1614
+ }
1615
+ const serverId = room.serverId;
1616
+ if (!serverId) {
1617
+ throw new Error("No server ID found 10");
1618
+ }
1619
+ const connection = getVoiceConnection(serverId);
1620
+ const agentName = state?.agentName || "The agent";
1621
+ if (!connection) {
1622
+ return {
1623
+ data: {
1624
+ isInVoiceChannel: false,
1625
+ room,
1626
+ serverId
1627
+ },
1628
+ values: {
1629
+ isInVoiceChannel: "false",
1630
+ serverId
1631
+ },
1632
+ text: `${agentName} is not currently in a voice channel`
1633
+ };
1634
+ }
1635
+ const worldId = room.worldId;
1636
+ const world = await runtime.getWorld(worldId);
1637
+ if (!world) {
1638
+ throw new Error("No world found");
1639
+ }
1640
+ const worldName = world.name;
1641
+ const roomType = room.type;
1642
+ const channelId = room.channelId;
1643
+ const channelName = room.name;
1644
+ if (!channelId) {
1645
+ return {
1646
+ data: {
1647
+ isInVoiceChannel: true,
1648
+ room,
1649
+ serverId,
1650
+ world,
1651
+ connection
1652
+ },
1653
+ values: {
1654
+ isInVoiceChannel: "true",
1655
+ serverId,
1656
+ worldName,
1657
+ roomType
1658
+ },
1659
+ text: `${agentName} is in an invalid voice channel`
1660
+ };
1661
+ }
1662
+ return {
1663
+ data: {
1664
+ isInVoiceChannel: true,
1665
+ room,
1666
+ serverId,
1667
+ world,
1668
+ connection,
1669
+ channelId,
1670
+ channelName
1671
+ },
1672
+ values: {
1673
+ isInVoiceChannel: "true",
1674
+ serverId,
1675
+ worldName,
1676
+ roomType,
1677
+ channelId,
1678
+ channelName
1679
+ },
1680
+ text: `${agentName} is currently in the voice channel: ${channelName} (ID: ${channelId})`
1681
+ };
1682
+ }
1683
+ };
1684
+
1685
+ // src/service.ts
1686
+ import { ChannelType as ChannelType9, EventTypes as EventTypes2, Role, Service, createUniqueUuid as createUniqueUuid6, logger as logger6 } from "@elizaos/core";
1687
+ import { ChannelType as DiscordChannelType4, Client as DiscordJsClient, Events, GatewayIntentBits, Partials, PermissionsBitField as PermissionsBitField2 } from "discord.js";
1688
+
1522
1689
  // src/constants.ts
1523
1690
  var MESSAGE_CONSTANTS = {
1524
1691
  MAX_MESSAGES: 10,
@@ -1535,16 +1702,18 @@ var DISCORD_SERVICE_NAME = "discord";
1535
1702
 
1536
1703
  // src/messages.ts
1537
1704
  import {
1538
- ChannelType as ChannelType5,
1705
+ ChannelType as ChannelType7,
1706
+ EventTypes,
1707
+ ServiceTypes as ServiceTypes4,
1539
1708
  createUniqueUuid as createUniqueUuid4,
1540
- logger as logger4,
1541
- ServiceTypes as ServiceTypes4
1709
+ logger as logger4
1542
1710
  } from "@elizaos/core";
1543
1711
  import {
1544
1712
  ChannelType as DiscordChannelType2
1545
1713
  } from "discord.js";
1546
1714
 
1547
1715
  // src/attachments.ts
1716
+ import fs3 from "node:fs";
1548
1717
  import { trimTokens as trimTokens3 } from "@elizaos/core";
1549
1718
  import { parseJSONObjectFromText as parseJSONObjectFromText5 } from "@elizaos/core";
1550
1719
  import {
@@ -1553,7 +1722,6 @@ import {
1553
1722
  } from "@elizaos/core";
1554
1723
  import { Collection } from "discord.js";
1555
1724
  import ffmpeg from "fluent-ffmpeg";
1556
- import fs3 from "node:fs";
1557
1725
  async function generateSummary(runtime, text) {
1558
1726
  text = await trimTokens3(text, 1e5, runtime);
1559
1727
  const prompt = `Please generate a concise summary for the following text:
@@ -1587,9 +1755,19 @@ async function generateSummary(runtime, text) {
1587
1755
  var AttachmentManager = class {
1588
1756
  attachmentCache = /* @__PURE__ */ new Map();
1589
1757
  runtime;
1758
+ /**
1759
+ * Constructor for creating a new instance of the class.
1760
+ *
1761
+ * @param {IAgentRuntime} runtime The runtime object to be injected into the instance.
1762
+ */
1590
1763
  constructor(runtime) {
1591
1764
  this.runtime = runtime;
1592
1765
  }
1766
+ /**
1767
+ * Processes attachments and returns an array of Media objects.
1768
+ * @param {Collection<string, Attachment> | Attachment[]} attachments - The attachments to be processed
1769
+ * @returns {Promise<Media[]>} - An array of processed Media objects
1770
+ */
1593
1771
  async processAttachments(attachments) {
1594
1772
  const processedAttachments = [];
1595
1773
  const attachmentCollection = attachments instanceof Collection ? attachments : new Collection(attachments.map((att) => [att.id, att]));
@@ -1601,6 +1779,15 @@ var AttachmentManager = class {
1601
1779
  }
1602
1780
  return processedAttachments;
1603
1781
  }
1782
+ /**
1783
+ * Processes the provided attachment to generate a media object.
1784
+ * If the media for the attachment URL is already cached, it will return the cached media.
1785
+ * Otherwise, it will determine the type of attachment (PDF, text, audio, video, image, generic)
1786
+ * and call the corresponding processing method to generate the media object.
1787
+ *
1788
+ * @param attachment The attachment to process
1789
+ * @returns A promise that resolves to a Media object representing the attachment, or null if the attachment could not be processed
1790
+ */
1604
1791
  async processAttachment(attachment) {
1605
1792
  if (this.attachmentCache.has(attachment.url)) {
1606
1793
  return this.attachmentCache.get(attachment.url);
@@ -1624,6 +1811,11 @@ var AttachmentManager = class {
1624
1811
  }
1625
1812
  return media;
1626
1813
  }
1814
+ /**
1815
+ * Asynchronously processes an audio or video attachment provided as input and returns a Media object.
1816
+ * @param {Attachment} attachment - The attachment object containing information about the audio/video file.
1817
+ * @returns {Promise<Media>} A Promise that resolves to a Media object representing the processed audio/video attachment.
1818
+ */
1627
1819
  async processAudioVideoAttachment(attachment) {
1628
1820
  try {
1629
1821
  const response = await fetch(attachment.url);
@@ -1666,6 +1858,12 @@ var AttachmentManager = class {
1666
1858
  };
1667
1859
  }
1668
1860
  }
1861
+ /**
1862
+ * Extracts the audio stream from the provided MP4 data and converts it to MP3 format.
1863
+ *
1864
+ * @param {ArrayBuffer} mp4Data - The MP4 data to extract audio from
1865
+ * @returns {Promise<Buffer>} - A Promise that resolves with the converted audio data as a Buffer
1866
+ */
1669
1867
  async extractAudioFromMP4(mp4Data) {
1670
1868
  const tempMP4File = `temp_${Date.now()}.mp4`;
1671
1869
  const tempAudioFile = `temp_${Date.now()}.mp3`;
@@ -1689,6 +1887,17 @@ var AttachmentManager = class {
1689
1887
  }
1690
1888
  }
1691
1889
  }
1890
+ /**
1891
+ * Processes a PDF attachment by fetching the PDF file from the specified URL,
1892
+ * converting it to text, generating a summary, and returning a Media object
1893
+ * with the extracted information.
1894
+ * If an error occurs during processing, a placeholder Media object is returned
1895
+ * with an error message.
1896
+ *
1897
+ * @param {Attachment} attachment - The PDF attachment to process.
1898
+ * @returns {Promise<Media>} A promise that resolves to a Media object representing
1899
+ * the processed PDF attachment.
1900
+ */
1692
1901
  async processPdfAttachment(attachment) {
1693
1902
  try {
1694
1903
  const response = await fetch(attachment.url);
@@ -1715,6 +1924,11 @@ var AttachmentManager = class {
1715
1924
  };
1716
1925
  }
1717
1926
  }
1927
+ /**
1928
+ * Processes a plaintext attachment by fetching its content, generating a summary, and returning a Media object.
1929
+ * @param {Attachment} attachment - The attachment object to process.
1930
+ * @returns {Promise<Media>} A promise that resolves to a Media object representing the processed plaintext attachment.
1931
+ */
1718
1932
  async processPlaintextAttachment(attachment) {
1719
1933
  try {
1720
1934
  const response = await fetch(attachment.url);
@@ -1740,6 +1954,14 @@ var AttachmentManager = class {
1740
1954
  };
1741
1955
  }
1742
1956
  }
1957
+ /**
1958
+ * Process the image attachment by fetching description and title using the IMAGE_DESCRIPTION model.
1959
+ * If successful, returns a Media object populated with the details. If unsuccessful, creates a fallback
1960
+ * Media object and logs the error.
1961
+ *
1962
+ * @param {Attachment} attachment - The attachment object containing the image details.
1963
+ * @returns {Promise<Media>} A promise that resolves to a Media object.
1964
+ */
1743
1965
  async processImageAttachment(attachment) {
1744
1966
  try {
1745
1967
  const { description, title } = await this.runtime.useModel(
@@ -1759,6 +1981,12 @@ var AttachmentManager = class {
1759
1981
  return this.createFallbackImageMedia(attachment);
1760
1982
  }
1761
1983
  }
1984
+ /**
1985
+ * Creates a fallback Media object for image attachments that could not be recognized.
1986
+ *
1987
+ * @param {Attachment} attachment - The attachment object containing image details.
1988
+ * @returns {Media} - The fallback Media object with basic information about the image attachment.
1989
+ */
1762
1990
  createFallbackImageMedia(attachment) {
1763
1991
  return {
1764
1992
  id: attachment.id,
@@ -1769,6 +1997,12 @@ var AttachmentManager = class {
1769
1997
  text: `This is an image attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
1770
1998
  };
1771
1999
  }
2000
+ /**
2001
+ * Process a video attachment to extract video information.
2002
+ * @param {Attachment} attachment - The attachment object containing video information.
2003
+ * @returns {Promise<Media>} A promise that resolves to a Media object with video details.
2004
+ * @throws {Error} If video service is not available.
2005
+ */
1772
2006
  async processVideoAttachment(attachment) {
1773
2007
  const videoService = this.runtime.getService(
1774
2008
  ServiceTypes3.VIDEO
@@ -1799,6 +2033,11 @@ var AttachmentManager = class {
1799
2033
  text: "Video content not available"
1800
2034
  };
1801
2035
  }
2036
+ /**
2037
+ * Process a generic attachment and return a Media object with specified properties.
2038
+ * @param {Attachment} attachment - The attachment object to process.
2039
+ * @returns {Promise<Media>} A Promise that resolves to a Media object with specified properties.
2040
+ */
1802
2041
  async processGenericAttachment(attachment) {
1803
2042
  return {
1804
2043
  id: attachment.id,
@@ -1815,11 +2054,11 @@ var AttachmentManager = class {
1815
2054
  import {
1816
2055
  ModelTypes as ModelTypes7,
1817
2056
  logger as logger3,
1818
- trimTokens as trimTokens4,
1819
- parseJSONObjectFromText as parseJSONObjectFromText6
2057
+ parseJSONObjectFromText as parseJSONObjectFromText6,
2058
+ trimTokens as trimTokens4
1820
2059
  } from "@elizaos/core";
1821
2060
  import {
1822
- ChannelType as ChannelType4,
2061
+ ChannelType as ChannelType6,
1823
2062
  PermissionsBitField,
1824
2063
  ThreadChannel
1825
2064
  } from "discord.js";
@@ -1896,7 +2135,7 @@ function canSendMessage(channel) {
1896
2135
  reason: "No channel given"
1897
2136
  };
1898
2137
  }
1899
- if (channel.type === ChannelType4.DM) {
2138
+ if (channel.type === ChannelType6.DM) {
1900
2139
  return {
1901
2140
  canSend: true,
1902
2141
  reason: null
@@ -1940,12 +2179,21 @@ var MessageManager = class {
1940
2179
  runtime;
1941
2180
  attachmentManager;
1942
2181
  getChannelType;
2182
+ /**
2183
+ * Constructor for a new instance of MyClass.
2184
+ * @param {any} discordClient - The Discord client object.
2185
+ */
1943
2186
  constructor(discordClient) {
1944
2187
  this.client = discordClient.client;
1945
2188
  this.runtime = discordClient.runtime;
1946
2189
  this.attachmentManager = new AttachmentManager(this.runtime);
1947
2190
  this.getChannelType = discordClient.getChannelType;
1948
2191
  }
2192
+ /**
2193
+ * Handles incoming Discord messages and processes them accordingly.
2194
+ *
2195
+ * @param {DiscordMessage} message - The Discord message to be handled
2196
+ */
1949
2197
  async handleMessage(message) {
1950
2198
  if (this.runtime.character.settings?.discord?.allowedChannelIds && !this.runtime.character.settings.discord.allowedChannelIds.some(
1951
2199
  (id) => id === message.channel.id
@@ -1973,7 +2221,7 @@ var MessageManager = class {
1973
2221
  type = await this.getChannelType(message.channel);
1974
2222
  serverId = guild.id;
1975
2223
  } else {
1976
- type = ChannelType5.DM;
2224
+ type = ChannelType7.DM;
1977
2225
  serverId = void 0;
1978
2226
  }
1979
2227
  await this.runtime.ensureConnection({
@@ -2062,7 +2310,7 @@ var MessageManager = class {
2062
2310
  return [];
2063
2311
  }
2064
2312
  };
2065
- this.runtime.emitEvent(["DISCORD_MESSAGE_RECEIVED", "MESSAGE_RECEIVED"], {
2313
+ this.runtime.emitEvent(["DISCORD_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, EventTypes.MESSAGE_RECEIVED], {
2066
2314
  runtime: this.runtime,
2067
2315
  message: newMessage,
2068
2316
  callback
@@ -2071,6 +2319,13 @@ var MessageManager = class {
2071
2319
  console.error("Error handling message:", error);
2072
2320
  }
2073
2321
  }
2322
+ /**
2323
+ * Processes the message content, mentions, code blocks, attachments, and URLs to generate
2324
+ * processed content and media attachments.
2325
+ *
2326
+ * @param {DiscordMessage} message The message to process
2327
+ * @returns {Promise<{ processedContent: string; attachments: Media[] }>} Processed content and media attachments
2328
+ */
2074
2329
  async processMessage(message) {
2075
2330
  let processedContent = message.content;
2076
2331
  let attachments = [];
@@ -2152,6 +2407,13 @@ var MessageManager = class {
2152
2407
  }
2153
2408
  return { processedContent, attachments };
2154
2409
  }
2410
+ /**
2411
+ * Asynchronously fetches the bot's username and discriminator from Discord API.
2412
+ *
2413
+ * @param {string} botToken The token of the bot to authenticate the request
2414
+ * @returns {Promise<string>} A promise that resolves with the bot's username and discriminator
2415
+ * @throws {Error} If there is an error while fetching the bot details
2416
+ */
2155
2417
  async fetchBotName(botToken) {
2156
2418
  const url = "https://discord.com/api/v10/users/@me";
2157
2419
  const response = await fetch(url, {
@@ -2169,526 +2431,103 @@ var MessageManager = class {
2169
2431
  }
2170
2432
  };
2171
2433
 
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
2434
+ // src/voice.ts
2435
+ import { EventEmitter } from "node:events";
2436
+ import { pipeline } from "node:stream";
2352
2437
  import {
2353
- AudioPlayerStatus,
2354
2438
  NoSubscriberBehavior,
2439
+ StreamType,
2355
2440
  VoiceConnectionStatus,
2356
2441
  createAudioPlayer,
2357
2442
  createAudioResource,
2358
- entersState
2443
+ entersState,
2444
+ getVoiceConnections,
2445
+ joinVoiceChannel
2359
2446
  } from "@discordjs/voice";
2360
2447
  import {
2448
+ ChannelType as ChannelType8,
2361
2449
  ModelTypes as ModelTypes8,
2450
+ createUniqueUuid as createUniqueUuid5,
2362
2451
  logger as logger5
2363
2452
  } 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)
2453
+ import {
2454
+ ChannelType as DiscordChannelType3
2455
+ } from "discord.js";
2456
+ import prism from "prism-media";
2457
+ var DECODE_FRAME_SIZE = 1024;
2458
+ var DECODE_SAMPLE_RATE = 16e3;
2459
+ var AudioMonitor = class {
2460
+ readable;
2461
+ buffers = [];
2462
+ maxSize;
2463
+ lastFlagged = -1;
2464
+ ended = false;
2465
+ /**
2466
+ * Constructs an AudioMonitor instance.
2467
+ * @param {Readable} readable - The readable stream to monitor for audio data.
2468
+ * @param {number} maxSize - The maximum size of the audio buffer.
2469
+ * @param {function} onStart - The callback function to be called when audio starts.
2470
+ * @param {function} callback - The callback function to process audio data.
2471
+ */
2472
+ constructor(readable, maxSize, onStart, callback) {
2473
+ this.readable = readable;
2474
+ this.maxSize = maxSize;
2475
+ this.readable.on("data", (chunk) => {
2476
+ if (this.lastFlagged < 0) {
2477
+ this.lastFlagged = this.buffers.length;
2395
2478
  }
2396
- ];
2397
- }
2398
- async testCreatingDiscordClient(runtime) {
2399
- try {
2400
- this.discordClient = runtime.getService(
2401
- ServiceTypes2.DISCORD
2479
+ this.buffers.push(chunk);
2480
+ const currentSize = this.buffers.reduce(
2481
+ (acc, cur) => acc + cur.length,
2482
+ 0
2402
2483
  );
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
- });
2484
+ while (currentSize > this.maxSize) {
2485
+ this.buffers.shift();
2486
+ this.lastFlagged--;
2411
2487
  }
2412
- } catch (error) {
2413
- throw new Error(`Error in test creating Discord client: ${error}`);
2414
- }
2488
+ });
2489
+ this.readable.on("end", () => {
2490
+ logger5.log("AudioMonitor ended");
2491
+ this.ended = true;
2492
+ if (this.lastFlagged < 0) return;
2493
+ callback(this.getBufferFromStart());
2494
+ this.lastFlagged = -1;
2495
+ });
2496
+ this.readable.on("speakingStopped", () => {
2497
+ if (this.ended) return;
2498
+ logger5.log("Speaking stopped");
2499
+ if (this.lastFlagged < 0) return;
2500
+ callback(this.getBufferFromStart());
2501
+ });
2502
+ this.readable.on("speakingStarted", () => {
2503
+ if (this.ended) return;
2504
+ onStart();
2505
+ logger5.log("Speaking started");
2506
+ this.reset();
2507
+ });
2415
2508
  }
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");
2509
+ /**
2510
+ * Stops listening to "data", "end", "speakingStopped", and "speakingStarted" events on the readable stream.
2511
+ */
2512
+ stop() {
2513
+ this.readable.removeAllListeners("data");
2514
+ this.readable.removeAllListeners("end");
2515
+ this.readable.removeAllListeners("speakingStopped");
2516
+ this.readable.removeAllListeners("speakingStarted");
2688
2517
  }
2518
+ /**
2519
+ * Check if the item is flagged.
2520
+ * @returns {boolean} True if the item was flagged, false otherwise.
2521
+ */
2689
2522
  isFlagged() {
2690
2523
  return this.lastFlagged >= 0;
2691
2524
  }
2525
+ /**
2526
+ * Returns a Buffer containing all buffers starting from the last flagged index.
2527
+ * If the last flagged index is less than 0, returns null.
2528
+ *
2529
+ * @returns {Buffer | null} The concatenated Buffer or null
2530
+ */
2692
2531
  getBufferFromFlag() {
2693
2532
  if (this.lastFlagged < 0) {
2694
2533
  return null;
@@ -2696,14 +2535,26 @@ var AudioMonitor = class {
2696
2535
  const buffer = Buffer.concat(this.buffers.slice(this.lastFlagged));
2697
2536
  return buffer;
2698
2537
  }
2538
+ /**
2539
+ * Concatenates all buffers in the array and returns a single buffer.
2540
+ *
2541
+ * @returns {Buffer} The concatenated buffer from the start.
2542
+ */
2699
2543
  getBufferFromStart() {
2700
2544
  const buffer = Buffer.concat(this.buffers);
2701
2545
  return buffer;
2702
2546
  }
2547
+ /**
2548
+ * Resets the buffers array and sets lastFlagged to -1.
2549
+ */
2703
2550
  reset() {
2704
2551
  this.buffers = [];
2705
2552
  this.lastFlagged = -1;
2706
2553
  }
2554
+ /**
2555
+ * Check if the object has ended.
2556
+ * @returns {boolean} Returns true if the object has ended; false otherwise.
2557
+ */
2707
2558
  isEnded() {
2708
2559
  return this.ended;
2709
2560
  }
@@ -2719,6 +2570,12 @@ var VoiceManager = class extends EventEmitter {
2719
2570
  connections = /* @__PURE__ */ new Map();
2720
2571
  activeMonitors = /* @__PURE__ */ new Map();
2721
2572
  ready;
2573
+ /**
2574
+ * Constructor for initializing a new instance of the class.
2575
+ *
2576
+ * @param {DiscordService} service - The Discord service to use.
2577
+ * @param {IAgentRuntime} runtime - The runtime for the agent.
2578
+ */
2722
2579
  constructor(service, runtime) {
2723
2580
  super();
2724
2581
  this.client = service.client;
@@ -2727,21 +2584,41 @@ var VoiceManager = class extends EventEmitter {
2727
2584
  this.setReady(true);
2728
2585
  });
2729
2586
  }
2587
+ /**
2588
+ * Asynchronously retrieves the type of the channel.
2589
+ * @param {Channel} channel - The channel to get the type for.
2590
+ * @returns {Promise<ChannelType>} The type of the channel.
2591
+ */
2730
2592
  async getChannelType(channel) {
2731
2593
  switch (channel.type) {
2732
2594
  case DiscordChannelType3.GuildVoice:
2733
2595
  case DiscordChannelType3.GuildStageVoice:
2734
- return ChannelType9.VOICE_GROUP;
2596
+ return ChannelType8.VOICE_GROUP;
2735
2597
  }
2736
2598
  }
2599
+ /**
2600
+ * Set the ready status of the VoiceManager.
2601
+ * @param {boolean} status - The status to set.
2602
+ */
2737
2603
  setReady(status) {
2738
2604
  this.ready = status;
2739
2605
  this.emit("ready");
2740
- logger6.success(`VoiceManager is now ready: ${this.ready}`);
2606
+ logger5.debug(`VoiceManager is now ready: ${this.ready}`);
2741
2607
  }
2608
+ /**
2609
+ * Check if the object is ready.
2610
+ *
2611
+ * @returns {boolean} True if the object is ready, false otherwise.
2612
+ */
2742
2613
  isReady() {
2743
2614
  return this.ready;
2744
2615
  }
2616
+ /**
2617
+ * Handle voice state update event.
2618
+ * @param {VoiceState} oldState - The old voice state of the member.
2619
+ * @param {VoiceState} newState - The new voice state of the member.
2620
+ * @returns {void}
2621
+ */
2745
2622
  async handleVoiceStateUpdate(oldState, newState) {
2746
2623
  const oldChannelId = oldState.channelId;
2747
2624
  const newChannelId = newState.channelId;
@@ -2763,6 +2640,10 @@ var VoiceManager = class extends EventEmitter {
2763
2640
  );
2764
2641
  }
2765
2642
  }
2643
+ /**
2644
+ * Joins a voice channel and sets up the necessary connection and event listeners.
2645
+ * @param {BaseGuildVoiceChannel} channel - The voice channel to join
2646
+ */
2766
2647
  async joinChannel(channel) {
2767
2648
  const oldConnection = this.getVoiceConnection(channel.guildId);
2768
2649
  if (oldConnection) {
@@ -2784,38 +2665,38 @@ var VoiceManager = class extends EventEmitter {
2784
2665
  });
2785
2666
  try {
2786
2667
  await Promise.race([
2787
- entersState2(connection, VoiceConnectionStatus2.Ready, 2e4),
2788
- entersState2(connection, VoiceConnectionStatus2.Signalling, 2e4)
2668
+ entersState(connection, VoiceConnectionStatus.Ready, 2e4),
2669
+ entersState(connection, VoiceConnectionStatus.Signalling, 2e4)
2789
2670
  ]);
2790
- logger6.log(
2671
+ logger5.log(
2791
2672
  `Voice connection established in state: ${connection.state.status}`
2792
2673
  );
2793
2674
  connection.on("stateChange", async (oldState, newState) => {
2794
- logger6.log(
2675
+ logger5.log(
2795
2676
  `Voice connection state changed from ${oldState.status} to ${newState.status}`
2796
2677
  );
2797
- if (newState.status === VoiceConnectionStatus2.Disconnected) {
2798
- logger6.log("Handling disconnection...");
2678
+ if (newState.status === VoiceConnectionStatus.Disconnected) {
2679
+ logger5.log("Handling disconnection...");
2799
2680
  try {
2800
2681
  await Promise.race([
2801
- entersState2(connection, VoiceConnectionStatus2.Signalling, 5e3),
2802
- entersState2(connection, VoiceConnectionStatus2.Connecting, 5e3)
2682
+ entersState(connection, VoiceConnectionStatus.Signalling, 5e3),
2683
+ entersState(connection, VoiceConnectionStatus.Connecting, 5e3)
2803
2684
  ]);
2804
- logger6.log("Reconnecting to channel...");
2685
+ logger5.log("Reconnecting to channel...");
2805
2686
  } catch (e) {
2806
- logger6.log(`Disconnection confirmed - cleaning up...${e}`);
2687
+ logger5.log(`Disconnection confirmed - cleaning up...${e}`);
2807
2688
  connection.destroy();
2808
2689
  this.connections.delete(channel.id);
2809
2690
  }
2810
- } else if (newState.status === VoiceConnectionStatus2.Destroyed) {
2691
+ } else if (newState.status === VoiceConnectionStatus.Destroyed) {
2811
2692
  this.connections.delete(channel.id);
2812
- } else if (!this.connections.has(channel.id) && (newState.status === VoiceConnectionStatus2.Ready || newState.status === VoiceConnectionStatus2.Signalling)) {
2693
+ } else if (!this.connections.has(channel.id) && (newState.status === VoiceConnectionStatus.Ready || newState.status === VoiceConnectionStatus.Signalling)) {
2813
2694
  this.connections.set(channel.id, connection);
2814
2695
  }
2815
2696
  });
2816
2697
  connection.on("error", (error) => {
2817
- logger6.log("Voice connection error:", error);
2818
- logger6.log("Connection error - will attempt to recover...");
2698
+ logger5.log("Voice connection error:", error);
2699
+ logger5.log("Connection error - will attempt to recover...");
2819
2700
  });
2820
2701
  this.connections.set(channel.id, connection);
2821
2702
  const me = channel.guild.members.me;
@@ -2824,7 +2705,7 @@ var VoiceManager = class extends EventEmitter {
2824
2705
  await me.voice.setDeaf(false);
2825
2706
  await me.voice.setMute(false);
2826
2707
  } catch (error) {
2827
- logger6.log("Failed to modify voice state:", error);
2708
+ logger5.log("Failed to modify voice state:", error);
2828
2709
  }
2829
2710
  }
2830
2711
  connection.receiver.speaking.on("start", async (entityId) => {
@@ -2848,12 +2729,17 @@ var VoiceManager = class extends EventEmitter {
2848
2729
  }
2849
2730
  });
2850
2731
  } catch (error) {
2851
- logger6.log("Failed to establish voice connection:", error);
2732
+ logger5.log("Failed to establish voice connection:", error);
2852
2733
  connection.destroy();
2853
2734
  this.connections.delete(channel.id);
2854
2735
  throw error;
2855
2736
  }
2856
2737
  }
2738
+ /**
2739
+ * Retrieves the voice connection for a given guild ID.
2740
+ * @param {string} guildId - The ID of the guild to get the voice connection for.
2741
+ * @returns {VoiceConnection | undefined} The voice connection for the specified guild ID, or undefined if not found.
2742
+ */
2857
2743
  getVoiceConnection(guildId) {
2858
2744
  const connections = getVoiceConnections(this.client.user.id);
2859
2745
  if (!connections) {
@@ -2864,6 +2750,12 @@ var VoiceManager = class extends EventEmitter {
2864
2750
  );
2865
2751
  return connection;
2866
2752
  }
2753
+ /**
2754
+ * Monitor a member's audio stream for volume activity and speaking thresholds.
2755
+ *
2756
+ * @param {GuildMember} member - The member whose audio stream is being monitored.
2757
+ * @param {BaseGuildVoiceChannel} channel - The voice channel in which the member is connected.
2758
+ */
2867
2759
  async monitorMember(member, channel) {
2868
2760
  const entityId = member?.id;
2869
2761
  const userName = member?.user?.username;
@@ -2944,6 +2836,12 @@ var VoiceManager = class extends EventEmitter {
2944
2836
  opusDecoder
2945
2837
  );
2946
2838
  }
2839
+ /**
2840
+ * Leaves the specified voice channel and stops monitoring all members in that channel.
2841
+ * If there is an active connection in the channel, it will be destroyed.
2842
+ *
2843
+ * @param {BaseGuildVoiceChannel} channel - The voice channel to leave.
2844
+ */
2947
2845
  leaveChannel(channel) {
2948
2846
  const connection = this.connections.get(channel.id);
2949
2847
  if (connection) {
@@ -2957,6 +2855,10 @@ var VoiceManager = class extends EventEmitter {
2957
2855
  }
2958
2856
  console.log(`Left voice channel: ${channel.name} (${channel.id})`);
2959
2857
  }
2858
+ /**
2859
+ * Stop monitoring a specific member by their member ID.
2860
+ * @param {string} memberId - The ID of the member to stop monitoring.
2861
+ */
2960
2862
  stopMonitoringMember(memberId) {
2961
2863
  const monitorInfo = this.activeMonitors.get(memberId);
2962
2864
  if (monitorInfo) {
@@ -2966,10 +2868,18 @@ var VoiceManager = class extends EventEmitter {
2966
2868
  console.log(`Stopped monitoring user ${memberId}`);
2967
2869
  }
2968
2870
  }
2871
+ /**
2872
+ * Asynchronously debounces the process transcription function to prevent rapid execution.
2873
+ *
2874
+ * @param {UUID} entityId - The ID of the entity related to the transcription.
2875
+ * @param {string} name - The name of the entity for transcription.
2876
+ * @param {string} userName - The username of the user initiating the transcription.
2877
+ * @param {BaseGuildVoiceChannel} channel - The voice channel where the transcription is happening.
2878
+ */
2969
2879
  async debouncedProcessTranscription(entityId, name, userName, channel) {
2970
2880
  const DEBOUNCE_TRANSCRIPTION_THRESHOLD = 1500;
2971
2881
  if (this.activeAudioPlayer?.state?.status === "idle") {
2972
- logger6.log("Cleaning up idle audio player.");
2882
+ logger5.log("Cleaning up idle audio player.");
2973
2883
  this.cleanupAudioPlayer(this.activeAudioPlayer);
2974
2884
  }
2975
2885
  if (this.activeAudioPlayer || this.processingVoice) {
@@ -3000,6 +2910,15 @@ var VoiceManager = class extends EventEmitter {
3000
2910
  }
3001
2911
  }, DEBOUNCE_TRANSCRIPTION_THRESHOLD);
3002
2912
  }
2913
+ /**
2914
+ * Handle user audio stream for monitoring purposes.
2915
+ *
2916
+ * @param {UUID} userId - The unique identifier of the user.
2917
+ * @param {string} name - The name of the user.
2918
+ * @param {string} userName - The username of the user.
2919
+ * @param {BaseGuildVoiceChannel} channel - The voice channel the user is in.
2920
+ * @param {Readable} audioStream - The audio stream to monitor.
2921
+ */
3003
2922
  async handleUserStream(userId, name, userName, channel, audioStream) {
3004
2923
  const entityId = createUniqueUuid5(this.runtime, userId);
3005
2924
  console.log(`Starting audio monitor for user: ${entityId}`);
@@ -3039,6 +2958,16 @@ var VoiceManager = class extends EventEmitter {
3039
2958
  }
3040
2959
  );
3041
2960
  }
2961
+ /**
2962
+ * Process the transcription of audio data for a user.
2963
+ *
2964
+ * @param {UUID} entityId - The unique ID of the user entity.
2965
+ * @param {string} channelId - The ID of the channel where the transcription is taking place.
2966
+ * @param {BaseGuildVoiceChannel} channel - The voice channel where the user is speaking.
2967
+ * @param {string} name - The name of the user.
2968
+ * @param {string} userName - The username of the user.
2969
+ * @returns {Promise<void>}
2970
+ */
3042
2971
  async processTranscription(entityId, channelId, channel, name, userName) {
3043
2972
  const state = this.userStates.get(entityId);
3044
2973
  if (!state || state.buffers.length === 0) return;
@@ -3053,7 +2982,7 @@ var VoiceManager = class extends EventEmitter {
3053
2982
  const wavBuffer = await this.convertOpusToWav(inputBuffer);
3054
2983
  console.log("Starting transcription...");
3055
2984
  const transcriptionText = await this.runtime.useModel(
3056
- ModelTypes9.TRANSCRIPTION,
2985
+ ModelTypes8.TRANSCRIPTION,
3057
2986
  wavBuffer
3058
2987
  );
3059
2988
  if (transcriptionText && isValidTranscription(transcriptionText)) {
@@ -3076,6 +3005,17 @@ var VoiceManager = class extends EventEmitter {
3076
3005
  console.error(`Error transcribing audio for user ${entityId}:`, error);
3077
3006
  }
3078
3007
  }
3008
+ /**
3009
+ * Handles a voice message received in a Discord channel.
3010
+ *
3011
+ * @param {string} message - The message content.
3012
+ * @param {UUID} entityId - The entity ID associated with the message.
3013
+ * @param {string} channelId - The ID of the Discord channel where the message was received.
3014
+ * @param {BaseGuildVoiceChannel} channel - The Discord channel where the message was received.
3015
+ * @param {string} name - The name associated with the message.
3016
+ * @param {string} userName - The user name associated with the message.
3017
+ * @returns {Promise<{text: string, actions: string[]}>} Object containing the resulting text and actions.
3018
+ */
3079
3019
  async handleMessage(message, entityId, channelId, channel, name, userName) {
3080
3020
  try {
3081
3021
  if (!message || message.trim() === "" || message.length < 3) {
@@ -3134,7 +3074,7 @@ var VoiceManager = class extends EventEmitter {
3134
3074
  if (responseMemory.content.text?.trim()) {
3135
3075
  await this.runtime.getMemoryManager("messages").createMemory(responseMemory);
3136
3076
  const responseStream = await this.runtime.useModel(
3137
- ModelTypes9.TEXT_TO_SPEECH,
3077
+ ModelTypes8.TEXT_TO_SPEECH,
3138
3078
  content.text
3139
3079
  );
3140
3080
  if (responseStream) {
@@ -3159,6 +3099,12 @@ var VoiceManager = class extends EventEmitter {
3159
3099
  console.error("Error processing voice message:", error);
3160
3100
  }
3161
3101
  }
3102
+ /**
3103
+ * Asynchronously converts an Opus audio Buffer to a WAV audio Buffer.
3104
+ *
3105
+ * @param {Buffer} pcmBuffer - The Opus audio Buffer to convert to WAV.
3106
+ * @returns {Promise<Buffer>} A Promise that resolves with the converted WAV audio Buffer.
3107
+ */
3162
3108
  async convertOpusToWav(pcmBuffer) {
3163
3109
  try {
3164
3110
  const wavHeader = getWavHeader(pcmBuffer.length, DECODE_SAMPLE_RATE);
@@ -3169,6 +3115,11 @@ var VoiceManager = class extends EventEmitter {
3169
3115
  throw error;
3170
3116
  }
3171
3117
  }
3118
+ /**
3119
+ * Scans the given Discord guild to select a suitable voice channel to join.
3120
+ *
3121
+ * @param {Guild} guild The Discord guild to scan for voice channels.
3122
+ */
3172
3123
  async scanGuild(guild) {
3173
3124
  let chosenChannel = null;
3174
3125
  try {
@@ -3196,12 +3147,19 @@ var VoiceManager = class extends EventEmitter {
3196
3147
  console.log(`Joining channel: ${chosenChannel.name}`);
3197
3148
  await this.joinChannel(chosenChannel);
3198
3149
  } else {
3199
- console.warn("No suitable voice channel found to join.");
3150
+ logger5.debug("Warning: No suitable voice channel found to join.");
3200
3151
  }
3201
3152
  } catch (error) {
3202
3153
  console.error("Error selecting or joining a voice channel:", error);
3203
3154
  }
3204
3155
  }
3156
+ /**
3157
+ * Play an audio stream for a given entity ID.
3158
+ *
3159
+ * @param {UUID} entityId - The ID of the entity to play the audio for.
3160
+ * @param {Readable} audioStream - The audio stream to play.
3161
+ * @returns {void}
3162
+ */
3205
3163
  async playAudioStream(entityId, audioStream) {
3206
3164
  const connection = this.connections.get(entityId);
3207
3165
  if (connection == null) {
@@ -3209,15 +3167,15 @@ var VoiceManager = class extends EventEmitter {
3209
3167
  return;
3210
3168
  }
3211
3169
  this.cleanupAudioPlayer(this.activeAudioPlayer);
3212
- const audioPlayer = createAudioPlayer2({
3170
+ const audioPlayer = createAudioPlayer({
3213
3171
  behaviors: {
3214
- noSubscriber: NoSubscriberBehavior2.Pause
3172
+ noSubscriber: NoSubscriberBehavior.Pause
3215
3173
  }
3216
3174
  });
3217
3175
  this.activeAudioPlayer = audioPlayer;
3218
3176
  connection.subscribe(audioPlayer);
3219
3177
  const audioStartTime = Date.now();
3220
- const resource = createAudioResource2(audioStream, {
3178
+ const resource = createAudioResource(audioStream, {
3221
3179
  inputType: StreamType.Arbitrary
3222
3180
  });
3223
3181
  audioPlayer.play(resource);
@@ -3234,6 +3192,12 @@ var VoiceManager = class extends EventEmitter {
3234
3192
  }
3235
3193
  );
3236
3194
  }
3195
+ /**
3196
+ * Cleans up the provided audio player by stopping it, removing all listeners,
3197
+ * and resetting the active audio player if it matches the provided player.
3198
+ *
3199
+ * @param {AudioPlayer} audioPlayer - The audio player to be cleaned up.
3200
+ */
3237
3201
  cleanupAudioPlayer(audioPlayer) {
3238
3202
  if (!audioPlayer) return;
3239
3203
  audioPlayer.stop();
@@ -3242,6 +3206,12 @@ var VoiceManager = class extends EventEmitter {
3242
3206
  this.activeAudioPlayer = null;
3243
3207
  }
3244
3208
  }
3209
+ /**
3210
+ * Asynchronously handles the join channel command in an interaction.
3211
+ *
3212
+ * @param {any} interaction - The interaction object representing the user's input.
3213
+ * @returns {Promise<void>} - A promise that resolves once the join channel command is handled.
3214
+ */
3245
3215
  async handleJoinChannelCommand(interaction) {
3246
3216
  try {
3247
3217
  await interaction.deferReply();
@@ -3269,6 +3239,12 @@ var VoiceManager = class extends EventEmitter {
3269
3239
  await interaction.editReply("Failed to join the voice channel.").catch(console.error);
3270
3240
  }
3271
3241
  }
3242
+ /**
3243
+ * Handles the leave channel command by destroying the voice connection if it exists.
3244
+ *
3245
+ * @param {any} interaction The interaction object representing the command invocation.
3246
+ * @returns {void}
3247
+ */
3272
3248
  async handleLeaveChannelCommand(interaction) {
3273
3249
  const connection = this.getVoiceConnection(interaction.guildId);
3274
3250
  if (!connection) {
@@ -3285,7 +3261,7 @@ var VoiceManager = class extends EventEmitter {
3285
3261
  }
3286
3262
  };
3287
3263
 
3288
- // src/index.ts
3264
+ // src/service.ts
3289
3265
  var DiscordService = class _DiscordService extends Service {
3290
3266
  static serviceType = DISCORD_SERVICE_NAME;
3291
3267
  capabilityDescription = "The agent is able to send and receive messages on discord";
@@ -3293,49 +3269,91 @@ var DiscordService = class _DiscordService extends Service {
3293
3269
  character;
3294
3270
  messageManager;
3295
3271
  voiceManager;
3272
+ /**
3273
+ * Constructor for Discord client.
3274
+ * Initializes the Discord client with specified intents and partials,
3275
+ * sets up event listeners, and ensures all servers exist.
3276
+ *
3277
+ * @param {IAgentRuntime} runtime - The AgentRuntime instance
3278
+ */
3296
3279
  constructor(runtime) {
3297
3280
  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);
3281
+ const token = runtime.getSetting("DISCORD_API_TOKEN");
3282
+ if (!token || token.trim() === "") {
3283
+ logger6.warn(
3284
+ "Discord API Token not provided - Discord functionality will be unavailable"
3285
+ );
3286
+ this.client = null;
3287
+ return;
3288
+ }
3289
+ try {
3290
+ this.client = new DiscordJsClient({
3291
+ intents: [
3292
+ GatewayIntentBits.Guilds,
3293
+ GatewayIntentBits.GuildMembers,
3294
+ GatewayIntentBits.GuildPresences,
3295
+ GatewayIntentBits.DirectMessages,
3296
+ GatewayIntentBits.GuildVoiceStates,
3297
+ GatewayIntentBits.MessageContent,
3298
+ GatewayIntentBits.GuildMessages,
3299
+ GatewayIntentBits.DirectMessageTyping,
3300
+ GatewayIntentBits.GuildMessageTyping,
3301
+ GatewayIntentBits.GuildMessageReactions
3302
+ ],
3303
+ partials: [
3304
+ Partials.Channel,
3305
+ Partials.Message,
3306
+ Partials.User,
3307
+ Partials.Reaction
3308
+ ]
3309
+ });
3310
+ this.runtime = runtime;
3311
+ this.voiceManager = new VoiceManager(this, runtime);
3312
+ this.messageManager = new MessageManager(this);
3313
+ this.client.once(Events.ClientReady, this.onClientReady.bind(this));
3314
+ this.client.login(token).catch((error) => {
3315
+ logger6.error(`Failed to login to Discord: ${error.message}`);
3316
+ this.client = null;
3317
+ });
3318
+ this.setupEventListeners();
3319
+ const ensureAllServersExist = async (runtime2) => {
3320
+ const guilds = await this.client.guilds.fetch();
3321
+ for (const [, guild] of guilds) {
3322
+ await this.ensureAllChannelsExist(runtime2, guild);
3323
+ }
3324
+ };
3325
+ ensureAllServersExist(this.runtime);
3326
+ } catch (error) {
3327
+ logger6.error(`Error initializing Discord client: ${error.message}`);
3328
+ this.client = null;
3329
+ }
3332
3330
  }
3331
+ /**
3332
+ * Ensures that all channels exist in the database for a given guild.
3333
+ * @param {IAgentRuntime} runtime - The agent runtime object.
3334
+ * @param {OAuth2Guild} guild - The OAuth2Guild object for which channels need to be ensured.
3335
+ * @returns {Promise<void>} - A Promise that resolves once all channels are ensured.
3336
+ */
3333
3337
  async ensureAllChannelsExist(runtime, guild) {
3334
- const guildObj = await guild.fetch();
3335
- const guildChannels = await guild.fetch();
3338
+ let guildObj;
3339
+ let guildChannels;
3340
+ let retries = 3;
3341
+ while (retries > 0) {
3342
+ try {
3343
+ guildObj = await guild.fetch();
3344
+ guildChannels = await guild.fetch();
3345
+ break;
3346
+ } catch (error) {
3347
+ retries--;
3348
+ if (retries === 0) {
3349
+ throw error;
3350
+ }
3351
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3352
+ }
3353
+ }
3336
3354
  for (const [, channel] of guildChannels.channels.cache) {
3337
3355
  const roomId = createUniqueUuid6(this.runtime, channel.id);
3338
- const room = await runtime.getDatabaseAdapter().getRoom(roomId);
3356
+ const room = await runtime.getRoom(roomId);
3339
3357
  if (room) {
3340
3358
  continue;
3341
3359
  }
@@ -3352,595 +3370,1090 @@ var DiscordService = class _DiscordService extends Service {
3352
3370
  [ownerId]: Role.OWNER
3353
3371
  }
3354
3372
  }
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
- });
3373
+ });
3374
+ await runtime.ensureRoomExists({
3375
+ id: roomId,
3376
+ name: channel.name,
3377
+ source: "discord",
3378
+ type: ChannelType9.GROUP,
3379
+ channelId: channel.id,
3380
+ serverId: guild.id,
3381
+ worldId
3382
+ });
3383
+ }
3384
+ }
3385
+ /**
3386
+ * Set up event listeners for the client
3387
+ */
3388
+ setupEventListeners() {
3389
+ if (!this.client) {
3390
+ return;
3391
+ }
3392
+ this.client.on("messageCreate", (message) => {
3393
+ if (message.author.id === this.client?.user?.id || message.author.bot) {
3394
+ return;
3395
+ }
3396
+ try {
3397
+ this.messageManager.handleMessage(message);
3398
+ } catch (error) {
3399
+ logger6.error(`Error handling message: ${error}`);
3400
+ }
3401
+ });
3402
+ this.client.on("messageReactionAdd", async (reaction, user) => {
3403
+ if (user.id === this.client?.user?.id) {
3404
+ return;
3405
+ }
3406
+ try {
3407
+ await this.handleReactionAdd(reaction, user);
3408
+ } catch (error) {
3409
+ logger6.error(`Error handling reaction add: ${error}`);
3410
+ }
3411
+ });
3412
+ this.client.on("messageReactionRemove", async (reaction, user) => {
3413
+ if (user.id === this.client?.user?.id) {
3414
+ return;
3415
+ }
3416
+ try {
3417
+ await this.handleReactionRemove(reaction, user);
3418
+ } catch (error) {
3419
+ logger6.error(`Error handling reaction remove: ${error}`);
3420
+ }
3421
+ });
3422
+ this.client.on("guildCreate", async (guild) => {
3423
+ try {
3424
+ await this.handleGuildCreate(guild);
3425
+ } catch (error) {
3426
+ logger6.error(`Error handling guild create: ${error}`);
3427
+ }
3428
+ });
3429
+ this.client.on("guildMemberAdd", async (member) => {
3430
+ try {
3431
+ await this.handleGuildMemberAdd(member);
3432
+ } catch (error) {
3433
+ logger6.error(`Error handling guild member add: ${error}`);
3434
+ }
3435
+ });
3436
+ this.client.on("interactionCreate", async (interaction) => {
3437
+ try {
3438
+ await this.handleInteractionCreate(interaction);
3439
+ } catch (error) {
3440
+ logger6.error(`Error handling interaction: ${error}`);
3441
+ }
3442
+ });
3443
+ }
3444
+ /**
3445
+ * Handles the event when a new member joins a guild.
3446
+ *
3447
+ * @param {GuildMember} member - The GuildMember object representing the new member that joined the guild.
3448
+ * @returns {Promise<void>} - A Promise that resolves once the event handling is complete.
3449
+ */
3450
+ async handleGuildMemberAdd(member) {
3451
+ logger6.log(`New member joined: ${member.user.username}`);
3452
+ const guild = member.guild;
3453
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3454
+ this.runtime.emitEvent([EventTypes2.ENTITY_JOINED], {
3455
+ runtime: this.runtime,
3456
+ entityId: createUniqueUuid6(this.runtime, member.id),
3457
+ worldId: createUniqueUuid6(this.runtime, guild.id),
3458
+ source: "discord",
3459
+ metadata: {
3460
+ originalId: member.id,
3461
+ username: tag,
3462
+ displayName: member.displayName || member.user.username,
3463
+ roles: member.roles.cache.map((r) => r.name),
3464
+ joinedAt: member.joinedAt?.getTime()
3465
+ }
3466
+ });
3467
+ this.runtime.emitEvent(["DISCORD_USER_JOINED" /* ENTITY_JOINED */], {
3468
+ runtime: this.runtime,
3469
+ entityId: createUniqueUuid6(this.runtime, member.id),
3470
+ member,
3471
+ guild
3472
+ });
3473
+ }
3474
+ /**
3475
+ *
3476
+ * Start the Discord service
3477
+ * @param {IAgentRuntime} runtime - The runtime for the agent
3478
+ * @returns {Promise<DiscordService>} A promise that resolves to a DiscordService instance
3479
+ *
3480
+ */
3481
+ static async start(runtime) {
3482
+ const token = runtime.getSetting("DISCORD_API_TOKEN");
3483
+ if (!token || token.trim() === "") {
3484
+ throw new Error("Discord API Token not provided");
3485
+ }
3486
+ const maxRetries = 5;
3487
+ let retryCount = 0;
3488
+ let lastError = null;
3489
+ while (retryCount < maxRetries) {
3490
+ try {
3491
+ const service = new _DiscordService(runtime);
3492
+ if (!service.client) {
3493
+ throw new Error("Failed to initialize Discord client");
3494
+ }
3495
+ await new Promise((resolve, reject) => {
3496
+ const timeout = setTimeout(() => {
3497
+ reject(new Error("Discord client ready timeout"));
3498
+ }, 3e4);
3499
+ service.client?.once("ready", () => {
3500
+ clearTimeout(timeout);
3501
+ resolve();
3502
+ });
3503
+ });
3504
+ return service;
3505
+ } catch (error) {
3506
+ lastError = error instanceof Error ? error : new Error(String(error));
3507
+ logger6.error(`Discord initialization attempt ${retryCount + 1} failed: ${lastError.message}`);
3508
+ retryCount++;
3509
+ if (retryCount < maxRetries) {
3510
+ const delay = 2 ** retryCount * 1e3;
3511
+ logger6.info(`Retrying Discord initialization in ${delay / 1e3} seconds...`);
3512
+ await new Promise((resolve) => setTimeout(resolve, delay));
3513
+ }
3514
+ }
3515
+ }
3516
+ throw new Error(`Discord initialization failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
3517
+ }
3518
+ /**
3519
+ * Stops the Discord client associated with the given runtime.
3520
+ *
3521
+ * @param {IAgentRuntime} runtime - The runtime associated with the Discord client.
3522
+ * @returns {void}
3523
+ */
3524
+ static async stop(runtime) {
3525
+ const client = runtime.getService(DISCORD_SERVICE_NAME);
3526
+ if (!client) {
3527
+ logger6.error("DiscordService not found");
3528
+ return;
3529
+ }
3530
+ try {
3531
+ await client.stop();
3532
+ } catch (e) {
3533
+ logger6.error("client-discord instance stop err", e);
3534
+ }
3535
+ }
3536
+ /**
3537
+ * Asynchronously stops the client by destroying it.
3538
+ *
3539
+ * @returns {Promise<void>}
3540
+ */
3541
+ async stop() {
3542
+ await this.client?.destroy();
3543
+ }
3544
+ /**
3545
+ * Handle the event when the client is ready.
3546
+ * @param {Object} readyClient - The ready client object containing user information.
3547
+ * @param {string} readyClient.user.tag - The username and discriminator of the client user.
3548
+ * @param {string} readyClient.user.id - The user ID of the client.
3549
+ * @returns {Promise<void>}
3550
+ */
3551
+ async onClientReady(readyClient) {
3552
+ logger6.success(`DISCORD: Logged in as ${readyClient.user?.tag}`);
3553
+ const commands = [
3554
+ {
3555
+ name: "joinchannel",
3556
+ description: "Join a voice channel",
3557
+ options: [
3558
+ {
3559
+ name: "channel",
3560
+ type: 7,
3561
+ // CHANNEL type
3562
+ description: "The voice channel to join",
3563
+ required: true,
3564
+ channel_types: [2]
3565
+ // GuildVoice type
3566
+ }
3567
+ ]
3568
+ },
3569
+ {
3570
+ name: "leavechannel",
3571
+ description: "Leave the current voice channel"
3572
+ }
3573
+ ];
3574
+ try {
3575
+ await this.client?.application?.commands.set(commands);
3576
+ logger6.success("DISCORD: Slash commands registered");
3577
+ } catch (error) {
3578
+ console.error("Error registering slash commands:", error);
3579
+ }
3580
+ const requiredPermissions = [
3581
+ // Text Permissions
3582
+ PermissionsBitField2.Flags.ViewChannel,
3583
+ PermissionsBitField2.Flags.SendMessages,
3584
+ PermissionsBitField2.Flags.SendMessagesInThreads,
3585
+ PermissionsBitField2.Flags.CreatePrivateThreads,
3586
+ PermissionsBitField2.Flags.CreatePublicThreads,
3587
+ PermissionsBitField2.Flags.EmbedLinks,
3588
+ PermissionsBitField2.Flags.AttachFiles,
3589
+ PermissionsBitField2.Flags.AddReactions,
3590
+ PermissionsBitField2.Flags.UseExternalEmojis,
3591
+ PermissionsBitField2.Flags.UseExternalStickers,
3592
+ PermissionsBitField2.Flags.MentionEveryone,
3593
+ PermissionsBitField2.Flags.ManageMessages,
3594
+ PermissionsBitField2.Flags.ReadMessageHistory,
3595
+ // Voice Permissions
3596
+ PermissionsBitField2.Flags.Connect,
3597
+ PermissionsBitField2.Flags.Speak,
3598
+ PermissionsBitField2.Flags.UseVAD,
3599
+ PermissionsBitField2.Flags.PrioritySpeaker
3600
+ ].reduce((a, b) => a | b, 0n);
3601
+ logger6.success("Use this URL to add the bot to your server:");
3602
+ logger6.success(
3603
+ `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user?.id}&permissions=${requiredPermissions}&scope=bot%20applications.commands`
3604
+ );
3605
+ await this.onReady();
3606
+ }
3607
+ /**
3608
+ * Asynchronously retrieves the type of a given channel.
3609
+ *
3610
+ * @param {Channel} channel - The channel for which to determine the type.
3611
+ * @returns {Promise<ChannelType>} A Promise that resolves with the type of the channel.
3612
+ */
3613
+ async getChannelType(channel) {
3614
+ switch (channel.type) {
3615
+ case DiscordChannelType4.DM:
3616
+ return ChannelType9.DM;
3617
+ case DiscordChannelType4.GuildText:
3618
+ return ChannelType9.GROUP;
3619
+ case DiscordChannelType4.GuildVoice:
3620
+ return ChannelType9.VOICE_GROUP;
3621
+ }
3622
+ }
3623
+ /**
3624
+ * Handles the addition of a reaction on a message.
3625
+ *
3626
+ * @param {MessageReaction | PartialMessageReaction} reaction The reaction that was added.
3627
+ * @param {User | PartialUser} user The user who added the reaction.
3628
+ * @returns {void}
3629
+ */
3630
+ async handleReactionAdd(reaction, user) {
3631
+ try {
3632
+ logger6.log("Reaction added");
3633
+ if (!reaction || !user) {
3634
+ logger6.warn("Invalid reaction or user");
3635
+ return;
3636
+ }
3637
+ let emoji = reaction.emoji.name;
3638
+ if (!emoji && reaction.emoji.id) {
3639
+ emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`;
3640
+ }
3641
+ if (reaction.partial) {
3642
+ try {
3643
+ await reaction.fetch();
3644
+ } catch (error) {
3645
+ logger6.error("Failed to fetch partial reaction:", error);
3646
+ return;
3647
+ }
3648
+ }
3649
+ const timestamp = Date.now();
3650
+ const roomId = createUniqueUuid6(
3651
+ this.runtime,
3652
+ reaction.message.channel.id
3653
+ );
3654
+ const entityId = createUniqueUuid6(this.runtime, user.id);
3655
+ const reactionUUID = createUniqueUuid6(
3656
+ this.runtime,
3657
+ `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`
3658
+ );
3659
+ if (!entityId || !roomId) {
3660
+ logger6.error("Invalid user ID or room ID", {
3661
+ entityId,
3662
+ roomId
3663
+ });
3664
+ return;
3665
+ }
3666
+ const messageContent = reaction.message.content || "";
3667
+ const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3668
+ const reactionMessage = `*Added <${emoji}> to: "${truncatedContent}"*`;
3669
+ const userName = reaction.message.author?.username || "unknown";
3670
+ const name = reaction.message.author?.displayName || userName;
3671
+ await this.runtime.ensureConnection({
3672
+ entityId,
3673
+ roomId,
3674
+ userName,
3675
+ name,
3676
+ source: "discord",
3677
+ channelId: reaction.message.channel.id,
3678
+ serverId: reaction.message.guild?.id,
3679
+ type: await this.getChannelType(reaction.message.channel)
3680
+ });
3681
+ const inReplyTo = createUniqueUuid6(this.runtime, reaction.message.id);
3682
+ const memory = {
3683
+ id: reactionUUID,
3684
+ entityId,
3685
+ agentId: this.runtime.agentId,
3686
+ content: {
3687
+ // name,
3688
+ // userName,
3689
+ text: reactionMessage,
3690
+ source: "discord",
3691
+ inReplyTo,
3692
+ channelType: await this.getChannelType(
3693
+ reaction.message.channel
3694
+ )
3695
+ },
3696
+ roomId,
3697
+ createdAt: timestamp
3698
+ };
3699
+ const callback = async (content) => {
3700
+ if (!reaction.message.channel) {
3701
+ logger6.error("No channel found for reaction message");
3702
+ return;
3703
+ }
3704
+ await reaction.message.channel.send(content.text);
3705
+ return [];
3706
+ };
3707
+ this.runtime.emitEvent(
3708
+ ["DISCORD_REACTION_RECEIVED", "REACTION_RECEIVED"],
3709
+ {
3710
+ runtime: this.runtime,
3711
+ message: memory,
3712
+ callback
3713
+ }
3714
+ );
3715
+ } catch (error) {
3716
+ logger6.error("Error handling reaction:", error);
3717
+ }
3718
+ }
3719
+ /**
3720
+ * Handles the removal of a reaction on a message.
3721
+ *
3722
+ * @param {MessageReaction | PartialMessageReaction} reaction - The reaction that was removed.
3723
+ * @param {User | PartialUser} user - The user who removed the reaction.
3724
+ * @returns {Promise<void>} - A Promise that resolves after handling the reaction removal.
3725
+ */
3726
+ async handleReactionRemove(reaction, user) {
3727
+ try {
3728
+ logger6.log("Reaction removed");
3729
+ let emoji = reaction.emoji.name;
3730
+ if (!emoji && reaction.emoji.id) {
3731
+ emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`;
3732
+ }
3733
+ if (reaction.partial) {
3734
+ try {
3735
+ await reaction.fetch();
3736
+ } catch (error) {
3737
+ logger6.error(
3738
+ "Something went wrong when fetching the message:",
3739
+ error
3740
+ );
3741
+ return;
3742
+ }
3743
+ }
3744
+ const messageContent = reaction.message.content || "";
3745
+ const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3746
+ const reactionMessage = `*Removed <${emoji}> from: "${truncatedContent}"*`;
3747
+ const roomId = createUniqueUuid6(
3748
+ this.runtime,
3749
+ reaction.message.channel.id
3750
+ );
3751
+ const entityId = createUniqueUuid6(this.runtime, user.id);
3752
+ const timestamp = Date.now();
3753
+ const reactionUUID = createUniqueUuid6(
3754
+ this.runtime,
3755
+ `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`
3756
+ );
3757
+ const userName = reaction.message.author?.username || "unknown";
3758
+ const name = reaction.message.author?.displayName || userName;
3759
+ await this.runtime.ensureConnection({
3760
+ entityId,
3761
+ roomId,
3762
+ userName,
3763
+ name,
3764
+ source: "discord",
3765
+ channelId: reaction.message.channel.id,
3766
+ serverId: reaction.message.guild?.id,
3767
+ type: await this.getChannelType(reaction.message.channel)
3768
+ });
3769
+ const memory = {
3770
+ id: reactionUUID,
3771
+ entityId,
3772
+ agentId: this.runtime.agentId,
3773
+ content: {
3774
+ // name,
3775
+ // userName,
3776
+ text: reactionMessage,
3777
+ source: "discord",
3778
+ inReplyTo: createUniqueUuid6(this.runtime, reaction.message.id),
3779
+ channelType: await this.getChannelType(
3780
+ reaction.message.channel
3781
+ )
3782
+ },
3783
+ roomId,
3784
+ createdAt: Date.now()
3785
+ };
3786
+ const callback = async (content) => {
3787
+ if (!reaction.message.channel) {
3788
+ logger6.error("No channel found for reaction message");
3789
+ return;
3790
+ }
3791
+ await reaction.message.channel.send(content.text);
3792
+ return [];
3793
+ };
3794
+ this.runtime.emitEvent(["DISCORD_REACTION_RECEIVED" /* REACTION_RECEIVED */], {
3795
+ runtime: this.runtime,
3796
+ message: memory,
3797
+ callback
3798
+ });
3799
+ } catch (error) {
3800
+ logger6.error("Error handling reaction removal:", error);
3801
+ }
3802
+ }
3803
+ /**
3804
+ * Handles the event when the bot joins a guild. It logs the guild name, fetches additional information about the guild, scans the guild for voice data, creates standardized world data structure, generates unique IDs, and emits events to the runtime.
3805
+ * @param {Guild} guild - The guild that the bot has joined.
3806
+ * @returns {Promise<void>}
3807
+ */
3808
+ async handleGuildCreate(guild) {
3809
+ logger6.log(`Joined guild ${guild.name}`);
3810
+ const fullGuild = await guild.fetch();
3811
+ this.voiceManager.scanGuild(guild);
3812
+ const ownerId = createUniqueUuid6(this.runtime, fullGuild.ownerId);
3813
+ const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
3814
+ const standardizedData = {
3815
+ runtime: this.runtime,
3816
+ rooms: await this.buildStandardizedRooms(fullGuild, worldId),
3817
+ users: await this.buildStandardizedUsers(fullGuild),
3818
+ world: {
3819
+ id: worldId,
3820
+ name: fullGuild.name,
3821
+ agentId: this.runtime.agentId,
3822
+ serverId: fullGuild.id,
3823
+ metadata: {
3824
+ ownership: fullGuild.ownerId ? { ownerId } : void 0,
3825
+ roles: {
3826
+ [ownerId]: Role.OWNER
3827
+ }
3828
+ }
3829
+ },
3830
+ source: "discord"
3831
+ };
3832
+ this.runtime.emitEvent(["DISCORD_WORLD_JOINED" /* WORLD_JOINED */], {
3833
+ runtime: this.runtime,
3834
+ server: fullGuild,
3835
+ source: "discord"
3836
+ });
3837
+ this.runtime.emitEvent([EventTypes2.WORLD_JOINED], standardizedData);
3838
+ }
3839
+ /**
3840
+ * Handles interactions created by the user, specifically commands.
3841
+ * @param {any} interaction - The interaction object received
3842
+ * @returns {void}
3843
+ */
3844
+ async handleInteractionCreate(interaction) {
3845
+ if (!interaction.isCommand()) return;
3846
+ switch (interaction.commandName) {
3847
+ case "joinchannel":
3848
+ await this.voiceManager.handleJoinChannelCommand(interaction);
3849
+ break;
3850
+ case "leavechannel":
3851
+ await this.voiceManager.handleLeaveChannelCommand(interaction);
3852
+ break;
3853
+ }
3854
+ }
3855
+ /**
3856
+ * Builds a standardized list of rooms from Discord guild channels
3857
+ */
3858
+ /**
3859
+ * Build standardized rooms for a guild based on text and voice channels.
3860
+ *
3861
+ * @param {Guild} guild The guild to build rooms for.
3862
+ * @param {UUID} _worldId The ID of the world to associate with the rooms.
3863
+ * @returns {Promise<any[]>} An array of standardized room objects.
3864
+ */
3865
+ async buildStandardizedRooms(guild, _worldId) {
3866
+ const rooms = [];
3867
+ for (const [channelId, channel] of guild.channels.cache) {
3868
+ if (channel.type === DiscordChannelType4.GuildText || channel.type === DiscordChannelType4.GuildVoice) {
3869
+ const roomId = createUniqueUuid6(this.runtime, channelId);
3870
+ let channelType;
3871
+ switch (channel.type) {
3872
+ case DiscordChannelType4.GuildText:
3873
+ channelType = ChannelType9.GROUP;
3874
+ break;
3875
+ case DiscordChannelType4.GuildVoice:
3876
+ channelType = ChannelType9.VOICE_GROUP;
3877
+ break;
3878
+ default:
3879
+ channelType = ChannelType9.GROUP;
3880
+ }
3881
+ let participants = [];
3882
+ if (guild.memberCount < 1e3 && channel.type === DiscordChannelType4.GuildText) {
3883
+ try {
3884
+ participants = Array.from(guild.members.cache.values()).filter(
3885
+ (member) => channel.permissionsFor(member)?.has(PermissionsBitField2.Flags.ViewChannel)
3886
+ ).map((member) => createUniqueUuid6(this.runtime, member.id));
3887
+ } catch (error) {
3888
+ logger6.warn(
3889
+ `Failed to get participants for channel ${channel.name}:`,
3890
+ error
3891
+ );
3892
+ }
3893
+ }
3894
+ rooms.push({
3895
+ id: roomId,
3896
+ name: channel.name,
3897
+ type: channelType,
3898
+ channelId: channel.id,
3899
+ participants
3900
+ });
3901
+ }
3902
+ }
3903
+ return rooms;
3904
+ }
3905
+ /**
3906
+ * Builds a standardized list of users from Discord guild members
3907
+ */
3908
+ async buildStandardizedUsers(guild) {
3909
+ const entities = [];
3910
+ const botId = this.client?.user?.id;
3911
+ if (guild.memberCount > 1e3) {
3912
+ logger6.info(
3913
+ `Using optimized user sync for large guild ${guild.name} (${guild.memberCount} members)`
3914
+ );
3915
+ try {
3916
+ for (const [, member] of guild.members.cache) {
3917
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3918
+ if (member.id !== botId) {
3919
+ entities.push({
3920
+ id: createUniqueUuid6(this.runtime, member.id),
3921
+ names: Array.from(
3922
+ /* @__PURE__ */ new Set([
3923
+ member.user.username,
3924
+ member.displayName,
3925
+ member.user.globalName
3926
+ ])
3927
+ ),
3928
+ agentId: this.runtime.agentId,
3929
+ metadata: {
3930
+ default: {
3931
+ username: tag,
3932
+ name: member.displayName || member.user.username
3933
+ },
3934
+ discord: member.user.globalName ? {
3935
+ username: tag,
3936
+ name: member.displayName || member.user.username,
3937
+ globalName: member.user.globalName,
3938
+ userId: member.id
3939
+ } : {
3940
+ username: tag,
3941
+ name: member.displayName || member.user.username,
3942
+ userId: member.id
3943
+ }
3944
+ }
3945
+ });
3946
+ }
3947
+ }
3948
+ if (entities.length < 100) {
3949
+ logger6.info(`Adding online members for ${guild.name}`);
3950
+ const onlineMembers = await guild.members.fetch({ limit: 100 });
3951
+ for (const [, member] of onlineMembers) {
3952
+ if (member.id !== botId) {
3953
+ const entityId = createUniqueUuid6(this.runtime, member.id);
3954
+ if (!entities.some((u) => u.id === entityId)) {
3955
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3956
+ entities.push({
3957
+ id: entityId,
3958
+ names: Array.from(
3959
+ /* @__PURE__ */ new Set([
3960
+ member.user.username,
3961
+ member.displayName,
3962
+ member.user.globalName
3963
+ ])
3964
+ ),
3965
+ agentId: this.runtime.agentId,
3966
+ metadata: {
3967
+ default: {
3968
+ username: tag,
3969
+ name: member.displayName || member.user.username
3970
+ },
3971
+ discord: member.user.globalName ? {
3972
+ username: tag,
3973
+ name: member.displayName || member.user.username,
3974
+ globalName: member.user.globalName,
3975
+ userId: member.id
3976
+ } : {
3977
+ username: tag,
3978
+ name: member.displayName || member.user.username,
3979
+ userId: member.id
3980
+ }
3981
+ }
3982
+ });
3983
+ }
3984
+ }
3985
+ }
3986
+ }
3987
+ } catch (error) {
3988
+ logger6.error(`Error fetching members for ${guild.name}:`, error);
3989
+ }
3990
+ } else {
3991
+ try {
3992
+ let members = guild.members.cache;
3993
+ if (members.size === 0) {
3994
+ members = await guild.members.fetch();
3995
+ }
3996
+ for (const [, member] of members) {
3997
+ if (member.id !== botId) {
3998
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3999
+ entities.push({
4000
+ id: createUniqueUuid6(this.runtime, member.id),
4001
+ names: Array.from(
4002
+ /* @__PURE__ */ new Set([
4003
+ member.user.username,
4004
+ member.displayName,
4005
+ member.user.globalName
4006
+ ])
4007
+ ),
4008
+ agentId: this.runtime.agentId,
4009
+ metadata: {
4010
+ default: {
4011
+ username: tag,
4012
+ name: member.displayName || member.user.username
4013
+ },
4014
+ discord: member.user.globalName ? {
4015
+ username: tag,
4016
+ name: member.displayName || member.user.username,
4017
+ globalName: member.user.globalName,
4018
+ userId: member.id
4019
+ } : {
4020
+ username: tag,
4021
+ name: member.displayName || member.user.username,
4022
+ userId: member.id
4023
+ }
4024
+ }
4025
+ });
4026
+ }
4027
+ }
4028
+ } catch (error) {
4029
+ logger6.error(`Error fetching members for ${guild.name}:`, error);
4030
+ }
3365
4031
  }
4032
+ return entities;
3366
4033
  }
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);
4034
+ async onReady() {
4035
+ logger6.log("DISCORD ON READY");
4036
+ const guilds = await this.client?.guilds.fetch();
4037
+ for (const [, guild] of guilds) {
4038
+ const fullGuild = await guild.fetch();
4039
+ await this.voiceManager.scanGuild(fullGuild);
4040
+ setTimeout(async () => {
4041
+ const fullGuild2 = await guild.fetch();
4042
+ logger6.log("DISCORD SERVER CONNECTED", fullGuild2.name);
4043
+ this.runtime.emitEvent(["DISCORD_SERVER_CONNECTED" /* WORLD_CONNECTED */], {
4044
+ runtime: this.runtime,
4045
+ server: fullGuild2,
4046
+ source: "discord"
4047
+ });
4048
+ const worldId = createUniqueUuid6(this.runtime, fullGuild2.id);
4049
+ const ownerId = createUniqueUuid6(this.runtime, fullGuild2.ownerId);
4050
+ const standardizedData = {
4051
+ name: fullGuild2.name,
4052
+ runtime: this.runtime,
4053
+ rooms: await this.buildStandardizedRooms(fullGuild2, worldId),
4054
+ entities: await this.buildStandardizedUsers(fullGuild2),
4055
+ world: {
4056
+ id: worldId,
4057
+ name: fullGuild2.name,
4058
+ agentId: this.runtime.agentId,
4059
+ serverId: fullGuild2.id,
4060
+ metadata: {
4061
+ ownership: fullGuild2.ownerId ? { ownerId } : void 0,
4062
+ roles: {
4063
+ [ownerId]: Role.OWNER
4064
+ }
4065
+ }
4066
+ },
4067
+ source: "discord"
4068
+ };
4069
+ this.runtime.emitEvent([EventTypes2.WORLD_CONNECTED], standardizedData);
4070
+ }, 1e3);
3434
4071
  }
4072
+ this.client?.emit("voiceManagerReady");
3435
4073
  }
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 = [
4074
+ };
4075
+
4076
+ // src/tests.ts
4077
+ import {
4078
+ AudioPlayerStatus,
4079
+ NoSubscriberBehavior as NoSubscriberBehavior2,
4080
+ VoiceConnectionStatus as VoiceConnectionStatus2,
4081
+ createAudioPlayer as createAudioPlayer2,
4082
+ createAudioResource as createAudioResource2,
4083
+ entersState as entersState2
4084
+ } from "@discordjs/voice";
4085
+ import {
4086
+ ModelTypes as ModelTypes9,
4087
+ logger as logger7
4088
+ } from "@elizaos/core";
4089
+ import { ChannelType as ChannelType10, Events as Events2 } from "discord.js";
4090
+ var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
4091
+ var DiscordTestSuite = class {
4092
+ name = "discord";
4093
+ discordClient = null;
4094
+ tests;
4095
+ /**
4096
+ * Constructor for initializing the tests array with test cases to be executed.
4097
+ *
4098
+ * @constructor
4099
+ * @this {TestSuite}
4100
+ */
4101
+ constructor() {
4102
+ this.tests = [
3442
4103
  {
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
- ]
4104
+ name: "Initialize Discord Client",
4105
+ fn: this.testCreatingDiscordClient.bind(this)
3456
4106
  },
3457
4107
  {
3458
- name: "leavechannel",
3459
- description: "Leave the current voice channel"
4108
+ name: "Slash Commands - Join Voice",
4109
+ fn: this.testJoinVoiceSlashCommand.bind(this)
4110
+ },
4111
+ {
4112
+ name: "Voice Playback & TTS",
4113
+ fn: this.testTextToSpeechPlayback.bind(this)
4114
+ },
4115
+ {
4116
+ name: "Send Message with Attachments",
4117
+ fn: this.testSendingTextMessage.bind(this)
4118
+ },
4119
+ {
4120
+ name: "Handle Incoming Messages",
4121
+ fn: this.testHandlingMessage.bind(this)
4122
+ },
4123
+ {
4124
+ name: "Slash Commands - Leave Voice",
4125
+ fn: this.testLeaveVoiceSlashCommand.bind(this)
3460
4126
  }
3461
4127
  ];
4128
+ }
4129
+ /**
4130
+ * Asynchronously tests the creation of Discord client using the provided runtime.
4131
+ *
4132
+ * @param {IAgentRuntime} runtime - The agent runtime used to obtain the Discord service.
4133
+ * @returns {Promise<void>} - A Promise that resolves once the Discord client is ready.
4134
+ * @throws {Error} - If an error occurs while creating the Discord client.
4135
+ */
4136
+ async testCreatingDiscordClient(runtime) {
3462
4137
  try {
3463
- await this.client.application?.commands.set(commands);
3464
- logger7.success("Slash commands registered");
4138
+ this.discordClient = runtime.getService(
4139
+ ServiceTypes2.DISCORD
4140
+ );
4141
+ if (this.discordClient.client.isReady()) {
4142
+ logger7.success("DiscordService is already ready.");
4143
+ } else {
4144
+ logger7.info("Waiting for DiscordService to be ready...");
4145
+ await new Promise((resolve, reject) => {
4146
+ this.discordClient.client.once(Events2.ClientReady, resolve);
4147
+ this.discordClient.client.once(Events2.Error, reject);
4148
+ });
4149
+ }
3465
4150
  } catch (error) {
3466
- console.error("Error registering slash commands:", error);
4151
+ throw new Error(`Error in test creating Discord client: ${error}`);
3467
4152
  }
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
4153
  }
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;
4154
+ /**
4155
+ * Asynchronously tests the join voice slash command functionality.
4156
+ *
4157
+ * @param {IAgentRuntime} runtime - The runtime environment for the agent.
4158
+ * @returns {Promise<void>} - A promise that resolves once the test is complete.
4159
+ * @throws {Error} - If there is an error in executing the slash command test.
4160
+ */
4161
+ async testJoinVoiceSlashCommand(runtime) {
4162
+ try {
4163
+ await this.waitForVoiceManagerReady(this.discordClient);
4164
+ const channel = await this.getTestChannel(runtime);
4165
+ if (!channel || !channel.isTextBased()) {
4166
+ throw new Error("Invalid test channel for slash command test.");
4167
+ }
4168
+ const fakeJoinInteraction = {
4169
+ isCommand: () => true,
4170
+ commandName: "joinchannel",
4171
+ options: {
4172
+ get: (name) => name === "channel" ? { value: channel.id } : null
4173
+ },
4174
+ guild: channel.guild,
4175
+ deferReply: async () => {
4176
+ },
4177
+ editReply: async (message) => {
4178
+ logger7.info(`JoinChannel Slash Command Response: ${message}`);
4179
+ }
4180
+ };
4181
+ await this.discordClient.voiceManager.handleJoinChannelCommand(
4182
+ fakeJoinInteraction
4183
+ );
4184
+ logger7.success("Slash command test completed successfully.");
4185
+ } catch (error) {
4186
+ throw new Error(`Error in slash commands test: ${error}`);
4187
+ }
4188
+ }
4189
+ /**
4190
+ * Asynchronously tests the leave voice channel slash command.
4191
+ *
4192
+ * @param {IAgentRuntime} runtime - The Agent Runtime instance.
4193
+ * @returns {Promise<void>} A promise that resolves when the test is complete.
4194
+ */
4195
+ async testLeaveVoiceSlashCommand(runtime) {
4196
+ try {
4197
+ await this.waitForVoiceManagerReady(this.discordClient);
4198
+ const channel = await this.getTestChannel(runtime);
4199
+ if (!channel || !channel.isTextBased()) {
4200
+ throw new Error("Invalid test channel for slash command test.");
4201
+ }
4202
+ const fakeLeaveInteraction = {
4203
+ isCommand: () => true,
4204
+ commandName: "leavechannel",
4205
+ guildId: channel.guildId,
4206
+ reply: async (message) => {
4207
+ logger7.info(`LeaveChannel Slash Command Response: ${message}`);
4208
+ }
4209
+ };
4210
+ await this.discordClient.voiceManager.handleLeaveChannelCommand(
4211
+ fakeLeaveInteraction
4212
+ );
4213
+ logger7.success("Slash command test completed successfully.");
4214
+ } catch (error) {
4215
+ throw new Error(`Error in slash commands test: ${error}`);
3503
4216
  }
3504
4217
  }
3505
- async handleReactionAdd(reaction, user) {
4218
+ /**
4219
+ * Test Text to Speech playback.
4220
+ * @param {IAgentRuntime} runtime - The Agent Runtime instance.
4221
+ * @throws {Error} - If voice channel is invalid, voice connection fails to become ready, or no text to speech service found.
4222
+ */
4223
+ async testTextToSpeechPlayback(runtime) {
3506
4224
  try {
3507
- logger7.log("Reaction added");
3508
- if (!reaction || !user) {
3509
- logger7.warn("Invalid reaction or user");
3510
- return;
4225
+ await this.waitForVoiceManagerReady(this.discordClient);
4226
+ const channel = await this.getTestChannel(runtime);
4227
+ if (!channel || channel.type !== ChannelType10.GuildVoice) {
4228
+ throw new Error("Invalid voice channel.");
3511
4229
  }
3512
- let emoji = reaction.emoji.name;
3513
- if (!emoji && reaction.emoji.id) {
3514
- emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`;
4230
+ await this.discordClient.voiceManager.joinChannel(channel);
4231
+ const guild = await this.getActiveGuild(this.discordClient);
4232
+ const guildId = guild.id;
4233
+ const connection = this.discordClient.voiceManager.getVoiceConnection(guildId);
4234
+ try {
4235
+ await entersState2(connection, VoiceConnectionStatus2.Ready, 1e4);
4236
+ logger7.success(`Voice connection is ready in guild: ${guildId}`);
4237
+ } catch (error) {
4238
+ throw new Error(`Voice connection failed to become ready: ${error}`);
3515
4239
  }
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
- }
4240
+ let responseStream = null;
4241
+ try {
4242
+ responseStream = await runtime.useModel(
4243
+ ModelTypes9.TEXT_TO_SPEECH,
4244
+ `Hi! I'm ${runtime.character.name}! How are you doing today?`
4245
+ );
4246
+ } catch (_error) {
4247
+ throw new Error("No text to speech service found");
3523
4248
  }
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;
4249
+ if (!responseStream) {
4250
+ throw new Error("TTS response stream is null or undefined.");
3540
4251
  }
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
- )
4252
+ await this.playAudioStream(responseStream, connection);
4253
+ } catch (error) {
4254
+ throw new Error(`Error in TTS playback test: ${error}`);
4255
+ }
4256
+ }
4257
+ /**
4258
+ * Asynchronously tests sending a text message to a specified channel.
4259
+ *
4260
+ * @param {IAgentRuntime} runtime - The runtime for the agent.
4261
+ * @returns {Promise<void>} A Promise that resolves when the message is sent successfully.
4262
+ * @throws {Error} If there is an error in sending the text message.
4263
+ */
4264
+ async testSendingTextMessage(runtime) {
4265
+ try {
4266
+ const channel = await this.getTestChannel(runtime);
4267
+ await this.sendMessageToChannel(
4268
+ channel,
4269
+ "Testing Message",
4270
+ [TEST_IMAGE_URL]
4271
+ );
4272
+ } catch (error) {
4273
+ throw new Error(`Error in sending text message: ${error}`);
4274
+ }
4275
+ }
4276
+ /**
4277
+ * Asynchronously handles sending a test message using the given runtime and mock user data.
4278
+ *
4279
+ * @param {IAgentRuntime} runtime - The agent runtime object.
4280
+ * @returns {Promise<void>} A Promise that resolves once the message is handled.
4281
+ */
4282
+ async testHandlingMessage(runtime) {
4283
+ try {
4284
+ const channel = await this.getTestChannel(runtime);
4285
+ const fakeMessage = {
4286
+ content: `Hello, ${runtime.character.name}! How are you?`,
4287
+ author: {
4288
+ id: "mock-user-id",
4289
+ username: "MockUser",
4290
+ bot: false
3570
4291
  },
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 [];
4292
+ channel,
4293
+ id: "mock-message-id",
4294
+ createdTimestamp: Date.now(),
4295
+ mentions: {
4296
+ has: () => false
4297
+ },
4298
+ reference: null,
4299
+ attachments: []
3581
4300
  };
3582
- this.runtime.emitEvent(
3583
- ["DISCORD_REACTION_RECEIVED", "REACTION_RECEIVED"],
3584
- {
3585
- runtime: this.runtime,
3586
- message: memory,
3587
- callback
3588
- }
3589
- );
4301
+ await this.discordClient.messageManager.handleMessage(fakeMessage);
3590
4302
  } catch (error) {
3591
- logger7.error("Error handling reaction:", error);
4303
+ throw new Error(`Error in sending text message: ${error}`);
3592
4304
  }
3593
4305
  }
3594
- async handleReactionRemove(reaction, user) {
4306
+ // #############################
4307
+ // Utility Functions
4308
+ // #############################
4309
+ /**
4310
+ * Asynchronously retrieves the test channel associated with the provided runtime.
4311
+ *
4312
+ * @param {IAgentRuntime} runtime - The runtime object containing necessary information.
4313
+ * @returns {Promise<Channel>} The test channel retrieved from the Discord client.
4314
+ * @throws {Error} If no test channel is found.
4315
+ */
4316
+ async getTestChannel(runtime) {
4317
+ const channelId = this.validateChannelId(runtime);
4318
+ const channel = await this.discordClient.client.channels.fetch(channelId);
4319
+ if (!channel) throw new Error("no test channel found!");
4320
+ return channel;
4321
+ }
4322
+ /**
4323
+ * Async function to send a message to a text-based channel.
4324
+ *
4325
+ * @param {TextChannel} channel - The text-based channel the message is being sent to.
4326
+ * @param {string} messageContent - The content of the message being sent.
4327
+ * @param {any[]} files - An array of files to include in the message.
4328
+ * @throws {Error} If the channel is not a text-based channel or does not exist.
4329
+ * @throws {Error} If there is an error sending the message.
4330
+ */
4331
+ async sendMessageToChannel(channel, messageContent, files) {
3595
4332
  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
- }
4333
+ if (!channel || !channel.isTextBased()) {
4334
+ throw new Error(
4335
+ "Channel is not a text-based channel or does not exist."
4336
+ );
3611
4337
  }
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}`
4338
+ await sendMessageInChunks(
4339
+ channel,
4340
+ messageContent,
4341
+ null,
4342
+ files
3624
4343
  );
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)
4344
+ } catch (error) {
4345
+ throw new Error(`Error sending message: ${error}`);
4346
+ }
4347
+ }
4348
+ /**
4349
+ * Play an audio stream from a given response stream using the provided VoiceConnection.
4350
+ *
4351
+ * @param {any} responseStream - The response stream to play as audio.
4352
+ * @param {VoiceConnection} connection - The VoiceConnection to use for playing the audio.
4353
+ * @returns {Promise<void>} - A Promise that resolves when the TTS playback is finished.
4354
+ */
4355
+ async playAudioStream(responseStream, connection) {
4356
+ const audioPlayer = createAudioPlayer2({
4357
+ behaviors: {
4358
+ noSubscriber: NoSubscriberBehavior2.Pause
4359
+ }
4360
+ });
4361
+ const audioResource = createAudioResource2(responseStream);
4362
+ audioPlayer.play(audioResource);
4363
+ connection.subscribe(audioPlayer);
4364
+ logger7.success("TTS playback started successfully.");
4365
+ await new Promise((resolve, reject) => {
4366
+ audioPlayer.once(AudioPlayerStatus.Idle, () => {
4367
+ logger7.info("TTS playback finished.");
4368
+ resolve();
3636
4369
  });
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
4370
+ audioPlayer.once("error", (error) => {
4371
+ reject(error);
4372
+ throw new Error(`TTS playback error: ${error}`);
3666
4373
  });
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
4374
  });
3700
- this.runtime.emitEvent(["SERVER_JOINED"], standardizedData);
3701
4375
  }
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;
4376
+ /**
4377
+ * Retrieves the active guild where the bot is currently connected to a voice channel.
4378
+ *
4379
+ * @param {DiscordService} discordClient The DiscordService instance used to interact with the Discord API.
4380
+ * @returns {Promise<Guild>} The active guild where the bot is currently connected to a voice channel.
4381
+ * @throws {Error} If no active voice connection is found for the bot.
4382
+ */
4383
+ async getActiveGuild(discordClient) {
4384
+ const guilds = await discordClient.client.guilds.fetch();
4385
+ const fullGuilds = await Promise.all(guilds.map((guild) => guild.fetch()));
4386
+ const activeGuild = fullGuilds.find((g) => g.members.me?.voice.channelId);
4387
+ if (!activeGuild) {
4388
+ throw new Error("No active voice connection found for the bot.");
3711
4389
  }
4390
+ return activeGuild;
3712
4391
  }
3713
4392
  /**
3714
- * Builds a standardized list of rooms from Discord guild channels
4393
+ * Waits for the VoiceManager in the Discord client to be ready.
4394
+ *
4395
+ * @param {DiscordService} discordClient - The Discord client to check for VoiceManager readiness.
4396
+ * @throws {Error} If the Discord client is not initialized.
4397
+ * @returns {Promise<void>} A promise that resolves when the VoiceManager is ready.
3715
4398
  */
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
- }
4399
+ async waitForVoiceManagerReady(discordClient) {
4400
+ if (!discordClient) {
4401
+ throw new Error("Discord client is not initialized.");
4402
+ }
4403
+ if (!discordClient.voiceManager.isReady()) {
4404
+ await new Promise((resolve, reject) => {
4405
+ discordClient.voiceManager.once("ready", resolve);
4406
+ discordClient.voiceManager.once("error", reject);
4407
+ });
3753
4408
  }
3754
- return rooms;
3755
4409
  }
3756
4410
  /**
3757
- * Builds a standardized list of users from Discord guild members
4411
+ * Validates the Discord test channel ID by checking if it is set in the runtime or environment variables.
4412
+ * If the test channel ID is not set, an error is thrown.
4413
+ *
4414
+ * @param {IAgentRuntime} runtime The runtime object containing the settings and environment variables.
4415
+ * @returns {string} The validated Discord test channel ID.
3758
4416
  */
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)`
4417
+ validateChannelId(runtime) {
4418
+ const testChannelId = runtime.getSetting("DISCORD_TEST_CHANNEL_ID") || process.env.DISCORD_TEST_CHANNEL_ID;
4419
+ if (!testChannelId) {
4420
+ throw new Error(
4421
+ "DISCORD_TEST_CHANNEL_ID is not set. Please provide a valid channel ID in the environment variables."
3765
4422
  );
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
4423
  }
3923
- this.client.emit("voiceManagerReady");
4424
+ return testChannelId;
3924
4425
  }
3925
4426
  };
4427
+
4428
+ // src/index.ts
3926
4429
  var discordPlugin = {
3927
4430
  name: "discord",
3928
4431
  description: "Discord client plugin",
3929
4432
  services: [DiscordService],
3930
4433
  actions: [
3931
4434
  chatWithAttachments_default,
3932
- downloadMedia_default,
3933
- voiceJoin_default,
3934
- voiceLeave_default,
3935
- summarizeConversation_default,
3936
- transcribeMedia_default
4435
+ downloadMedia,
4436
+ joinVoice,
4437
+ leaveVoice,
4438
+ summarize,
4439
+ transcribeMedia
3937
4440
  ],
3938
- providers: [channelState_default, voiceState_default],
3939
- tests: [new DiscordTestSuite()]
4441
+ providers: [channelStateProvider, voiceStateProvider],
4442
+ tests: [new DiscordTestSuite()],
4443
+ init: async (config, runtime) => {
4444
+ const token = runtime.getSetting("DISCORD_API_TOKEN");
4445
+ if (!token || token.trim() === "") {
4446
+ logger8.warn(
4447
+ "Discord API Token not provided - Discord plugin is loaded but will not be functional"
4448
+ );
4449
+ logger8.warn(
4450
+ "To enable Discord functionality, please provide DISCORD_API_TOKEN in your .eliza/.env file"
4451
+ );
4452
+ }
4453
+ }
3940
4454
  };
3941
4455
  var index_default = discordPlugin;
3942
4456
  export {
3943
- DiscordService,
3944
4457
  index_default as default
3945
4458
  };
3946
4459
  //# sourceMappingURL=index.js.map