@elizaos/plugin-discord 1.0.0-alpha.31 → 1.0.0-alpha.33

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,20 +1,7 @@
1
1
  // src/index.ts
2
2
  import {
3
- ChannelType as ChannelType10,
4
- EventTypes as EventTypes2,
5
- Role,
6
- Service,
7
- createUniqueUuid as createUniqueUuid6,
8
- logger as logger7
3
+ logger as logger8
9
4
  } from "@elizaos/core";
10
- import {
11
- ChannelType as DiscordChannelType4,
12
- Client as DiscordJsClient,
13
- Events as Events2,
14
- GatewayIntentBits,
15
- Partials,
16
- PermissionsBitField as PermissionsBitField2
17
- } from "discord.js";
18
5
 
19
6
  // src/actions/chatWithAttachments.ts
20
7
  import fs from "node:fs";
@@ -66,7 +53,7 @@ var getAttachmentIds = async (runtime, _message, state) => {
66
53
  }
67
54
  return null;
68
55
  };
69
- var summarizeAction = {
56
+ var chatWithAttachments = {
70
57
  name: "CHAT_WITH_ATTACHMENTS",
71
58
  similes: [
72
59
  "CHAT_WITH_ATTACHMENT",
@@ -298,7 +285,7 @@ ${currentSummary.trim()}
298
285
  ]
299
286
  ]
300
287
  };
301
- var chatWithAttachments_default = summarizeAction;
288
+ var chatWithAttachments_default = chatWithAttachments;
302
289
 
303
290
  // src/actions/downloadMedia.ts
304
291
  import {
@@ -336,7 +323,7 @@ var getMediaUrl = async (runtime, _message, state) => {
336
323
  }
337
324
  return null;
338
325
  };
339
- var downloadMedia_default = {
326
+ var downloadMedia = {
340
327
  name: "DOWNLOAD_MEDIA",
341
328
  similes: [
342
329
  "DOWNLOAD_VIDEO",
@@ -534,7 +521,7 @@ var getDateRange = async (runtime, _message, state) => {
534
521
  }
535
522
  }
536
523
  };
537
- var summarizeAction2 = {
524
+ var summarize = {
538
525
  name: "SUMMARIZE_CONVERSATION",
539
526
  similes: [
540
527
  "RECAP",
@@ -782,7 +769,6 @@ ${currentSummary.trim()}
782
769
  ]
783
770
  ]
784
771
  };
785
- var summarizeConversation_default = summarizeAction2;
786
772
 
787
773
  // src/actions/transcribeMedia.ts
788
774
  import {
@@ -820,7 +806,7 @@ var getMediaAttachmentId = async (runtime, _message, state) => {
820
806
  }
821
807
  return null;
822
808
  };
823
- var transcribeMediaAction = {
809
+ var transcribeMedia = {
824
810
  name: "TRANSCRIBE_MEDIA",
825
811
  similes: [
826
812
  "TRANSCRIBE_AUDIO",
@@ -960,7 +946,6 @@ ${mediaTranscript.trim()}
960
946
  ]
961
947
  ]
962
948
  };
963
- var transcribeMedia_default = transcribeMediaAction;
964
949
 
965
950
  // src/actions/voiceJoin.ts
966
951
  import {
@@ -980,7 +965,7 @@ var ServiceTypes2 = {
980
965
  };
981
966
 
982
967
  // src/actions/voiceJoin.ts
983
- var voiceJoin_default = {
968
+ var joinVoice = {
984
969
  name: "JOIN_VOICE",
985
970
  similes: [
986
971
  "JOIN_VOICE",
@@ -1273,7 +1258,7 @@ import {
1273
1258
  logger as logger2
1274
1259
  } from "@elizaos/core";
1275
1260
  import { BaseGuildVoiceChannel } from "discord.js";
1276
- var voiceLeave_default = {
1261
+ var leaveVoice = {
1277
1262
  name: "LEAVE_VOICE",
1278
1263
  similes: [
1279
1264
  "LEAVE_VOICE",
@@ -1520,1279 +1505,1283 @@ var voiceLeave_default = {
1520
1505
  ]
1521
1506
  };
1522
1507
 
1523
- // src/constants.ts
1524
- var MESSAGE_CONSTANTS = {
1525
- MAX_MESSAGES: 10,
1526
- RECENT_MESSAGE_COUNT: 3,
1527
- CHAT_HISTORY_COUNT: 5,
1528
- INTEREST_DECAY_TIME: 5 * 60 * 1e3,
1529
- // 5 minutes
1530
- PARTIAL_INTEREST_DECAY: 3 * 60 * 1e3,
1531
- // 3 minutes
1532
- DEFAULT_SIMILARITY_THRESHOLD: 0.3,
1533
- DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.2
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
+ }
1534
1590
  };
1535
- var DISCORD_SERVICE_NAME = "discord";
1536
1591
 
1537
- // src/messages.ts
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/tests.ts
1538
1686
  import {
1539
- ChannelType as ChannelType5,
1540
- EventTypes,
1541
- ServiceTypes as ServiceTypes4,
1542
- createUniqueUuid as createUniqueUuid4,
1687
+ AudioPlayerStatus,
1688
+ NoSubscriberBehavior,
1689
+ VoiceConnectionStatus,
1690
+ createAudioPlayer,
1691
+ createAudioResource,
1692
+ entersState
1693
+ } from "@discordjs/voice";
1694
+ import {
1695
+ ModelTypes as ModelTypes7,
1543
1696
  logger as logger4
1544
1697
  } from "@elizaos/core";
1545
- import {
1546
- ChannelType as DiscordChannelType2
1547
- } from "discord.js";
1698
+ import { ChannelType as ChannelType7, Events } from "discord.js";
1548
1699
 
1549
- // src/attachments.ts
1550
- import fs3 from "node:fs";
1551
- import { trimTokens as trimTokens3 } from "@elizaos/core";
1552
- import { parseJSONObjectFromText as parseJSONObjectFromText5 } from "@elizaos/core";
1700
+ // src/utils.ts
1553
1701
  import {
1554
1702
  ModelTypes as ModelTypes6,
1555
- ServiceTypes as ServiceTypes3
1703
+ logger as logger3,
1704
+ parseJSONObjectFromText as parseJSONObjectFromText5,
1705
+ trimTokens as trimTokens3
1556
1706
  } from "@elizaos/core";
1557
- import { Collection } from "discord.js";
1558
- import ffmpeg from "fluent-ffmpeg";
1559
- async function generateSummary(runtime, text) {
1560
- text = await trimTokens3(text, 1e5, runtime);
1561
- const prompt = `Please generate a concise summary for the following text:
1562
-
1563
- Text: """
1564
- ${text}
1565
- """
1566
-
1567
- Respond with a JSON object in the following format:
1568
- \`\`\`json
1569
- {
1570
- "title": "Generated Title",
1571
- "summary": "Generated summary and/or description of the text"
1572
- }
1573
- \`\`\``;
1574
- const response = await runtime.useModel(ModelTypes6.TEXT_SMALL, {
1575
- prompt
1576
- });
1577
- const parsedResponse = parseJSONObjectFromText5(response);
1578
- if (parsedResponse?.title && parsedResponse?.summary) {
1579
- return {
1580
- title: parsedResponse.title,
1581
- description: parsedResponse.summary
1582
- };
1583
- }
1584
- return {
1585
- title: "",
1586
- description: ""
1587
- };
1707
+ import {
1708
+ ChannelType as ChannelType6,
1709
+ PermissionsBitField,
1710
+ ThreadChannel
1711
+ } from "discord.js";
1712
+ function getWavHeader(audioLength, sampleRate, channelCount = 1, bitsPerSample = 16) {
1713
+ const wavHeader = Buffer.alloc(44);
1714
+ wavHeader.write("RIFF", 0);
1715
+ wavHeader.writeUInt32LE(36 + audioLength, 4);
1716
+ wavHeader.write("WAVE", 8);
1717
+ wavHeader.write("fmt ", 12);
1718
+ wavHeader.writeUInt32LE(16, 16);
1719
+ wavHeader.writeUInt16LE(1, 20);
1720
+ wavHeader.writeUInt16LE(channelCount, 22);
1721
+ wavHeader.writeUInt32LE(sampleRate, 24);
1722
+ wavHeader.writeUInt32LE(sampleRate * bitsPerSample * channelCount / 8, 28);
1723
+ wavHeader.writeUInt16LE(bitsPerSample * channelCount / 8, 32);
1724
+ wavHeader.writeUInt16LE(bitsPerSample, 34);
1725
+ wavHeader.write("data", 36);
1726
+ wavHeader.writeUInt32LE(audioLength, 40);
1727
+ return wavHeader;
1588
1728
  }
1589
- var AttachmentManager = class {
1590
- attachmentCache = /* @__PURE__ */ new Map();
1591
- runtime;
1592
- /**
1593
- * Constructor for creating a new instance of the class.
1594
- *
1595
- * @param {IAgentRuntime} runtime The runtime object to be injected into the instance.
1596
- */
1597
- constructor(runtime) {
1598
- this.runtime = runtime;
1599
- }
1600
- /**
1601
- * Processes attachments and returns an array of Media objects.
1602
- * @param {Collection<string, Attachment> | Attachment[]} attachments - The attachments to be processed
1603
- * @returns {Promise<Media[]>} - An array of processed Media objects
1604
- */
1605
- async processAttachments(attachments) {
1606
- const processedAttachments = [];
1607
- const attachmentCollection = attachments instanceof Collection ? attachments : new Collection(attachments.map((att) => [att.id, att]));
1608
- for (const [, attachment] of attachmentCollection) {
1609
- const media = await this.processAttachment(attachment);
1610
- if (media) {
1611
- processedAttachments.push(media);
1729
+ var MAX_MESSAGE_LENGTH = 1900;
1730
+ async function sendMessageInChunks(channel, content, _inReplyTo, files) {
1731
+ const sentMessages = [];
1732
+ const messages = splitMessage(content);
1733
+ try {
1734
+ for (let i = 0; i < messages.length; i++) {
1735
+ const message = messages[i];
1736
+ if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0) {
1737
+ const options = {
1738
+ content: message.trim()
1739
+ };
1740
+ if (i === messages.length - 1 && files && files.length > 0) {
1741
+ options.files = files;
1742
+ }
1743
+ const m = await channel.send(options);
1744
+ sentMessages.push(m);
1612
1745
  }
1613
1746
  }
1614
- return processedAttachments;
1747
+ } catch (error) {
1748
+ logger3.error("Error sending message:", error);
1749
+ }
1750
+ return sentMessages;
1751
+ }
1752
+ function splitMessage(content) {
1753
+ const messages = [];
1754
+ let currentMessage = "";
1755
+ const rawLines = content?.split("\n") || [];
1756
+ const lines = rawLines.flatMap((line) => {
1757
+ const chunks = [];
1758
+ while (line.length > MAX_MESSAGE_LENGTH) {
1759
+ chunks.push(line.slice(0, MAX_MESSAGE_LENGTH));
1760
+ line = line.slice(MAX_MESSAGE_LENGTH);
1761
+ }
1762
+ chunks.push(line);
1763
+ return chunks;
1764
+ });
1765
+ for (const line of lines) {
1766
+ if (currentMessage.length + line.length + 1 > MAX_MESSAGE_LENGTH) {
1767
+ messages.push(currentMessage.trim());
1768
+ currentMessage = "";
1769
+ }
1770
+ currentMessage += `${line}
1771
+ `;
1615
1772
  }
1773
+ if (currentMessage.trim().length > 0) {
1774
+ messages.push(currentMessage.trim());
1775
+ }
1776
+ return messages;
1777
+ }
1778
+ function canSendMessage(channel) {
1779
+ if (!channel) {
1780
+ return {
1781
+ canSend: false,
1782
+ reason: "No channel given"
1783
+ };
1784
+ }
1785
+ if (channel.type === ChannelType6.DM) {
1786
+ return {
1787
+ canSend: true,
1788
+ reason: null
1789
+ };
1790
+ }
1791
+ const botMember = channel.guild?.members.cache.get(channel.client.user.id);
1792
+ if (!botMember) {
1793
+ return {
1794
+ canSend: false,
1795
+ reason: "Not a guild channel or bot member not found"
1796
+ };
1797
+ }
1798
+ const requiredPermissions = [
1799
+ PermissionsBitField.Flags.ViewChannel,
1800
+ PermissionsBitField.Flags.SendMessages,
1801
+ PermissionsBitField.Flags.ReadMessageHistory
1802
+ ];
1803
+ if (channel instanceof ThreadChannel) {
1804
+ requiredPermissions.push(PermissionsBitField.Flags.SendMessagesInThreads);
1805
+ }
1806
+ const permissions = channel.permissionsFor(botMember);
1807
+ if (!permissions) {
1808
+ return {
1809
+ canSend: false,
1810
+ reason: "Could not retrieve permissions"
1811
+ };
1812
+ }
1813
+ const missingPermissions = requiredPermissions.filter(
1814
+ (perm) => !permissions.has(perm)
1815
+ );
1816
+ return {
1817
+ canSend: missingPermissions.length === 0,
1818
+ missingPermissions,
1819
+ reason: missingPermissions.length > 0 ? `Missing permissions: ${missingPermissions.map((p) => String(p)).join(", ")}` : null
1820
+ };
1821
+ }
1822
+
1823
+ // src/tests.ts
1824
+ var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
1825
+ var DiscordTestSuite = class {
1826
+ name = "discord";
1827
+ discordClient = null;
1828
+ tests;
1616
1829
  /**
1617
- * Processes the provided attachment to generate a media object.
1618
- * If the media for the attachment URL is already cached, it will return the cached media.
1619
- * Otherwise, it will determine the type of attachment (PDF, text, audio, video, image, generic)
1620
- * and call the corresponding processing method to generate the media object.
1830
+ * Constructor for initializing the tests array with test cases to be executed.
1621
1831
  *
1622
- * @param attachment The attachment to process
1623
- * @returns A promise that resolves to a Media object representing the attachment, or null if the attachment could not be processed
1832
+ * @constructor
1833
+ * @this {TestSuite}
1624
1834
  */
1625
- async processAttachment(attachment) {
1626
- if (this.attachmentCache.has(attachment.url)) {
1627
- return this.attachmentCache.get(attachment.url);
1628
- }
1629
- let media = null;
1630
- if (attachment.contentType?.startsWith("application/pdf")) {
1631
- media = await this.processPdfAttachment(attachment);
1632
- } else if (attachment.contentType?.startsWith("text/plain")) {
1633
- media = await this.processPlaintextAttachment(attachment);
1634
- } else if (attachment.contentType?.startsWith("audio/") || attachment.contentType?.startsWith("video/mp4")) {
1635
- media = await this.processAudioVideoAttachment(attachment);
1636
- } else if (attachment.contentType?.startsWith("image/")) {
1637
- media = await this.processImageAttachment(attachment);
1638
- } else if (attachment.contentType?.startsWith("video/") || this.runtime.getService(ServiceTypes3.VIDEO).isVideoUrl(attachment.url)) {
1639
- media = await this.processVideoAttachment(attachment);
1640
- } else {
1641
- media = await this.processGenericAttachment(attachment);
1642
- }
1643
- if (media) {
1644
- this.attachmentCache.set(attachment.url, media);
1645
- }
1646
- return media;
1835
+ constructor() {
1836
+ this.tests = [
1837
+ {
1838
+ name: "Initialize Discord Client",
1839
+ fn: this.testCreatingDiscordClient.bind(this)
1840
+ },
1841
+ {
1842
+ name: "Slash Commands - Join Voice",
1843
+ fn: this.testJoinVoiceSlashCommand.bind(this)
1844
+ },
1845
+ {
1846
+ name: "Voice Playback & TTS",
1847
+ fn: this.testTextToSpeechPlayback.bind(this)
1848
+ },
1849
+ {
1850
+ name: "Send Message with Attachments",
1851
+ fn: this.testSendingTextMessage.bind(this)
1852
+ },
1853
+ {
1854
+ name: "Handle Incoming Messages",
1855
+ fn: this.testHandlingMessage.bind(this)
1856
+ },
1857
+ {
1858
+ name: "Slash Commands - Leave Voice",
1859
+ fn: this.testLeaveVoiceSlashCommand.bind(this)
1860
+ }
1861
+ ];
1647
1862
  }
1648
1863
  /**
1649
- * Asynchronously processes an audio or video attachment provided as input and returns a Media object.
1650
- * @param {Attachment} attachment - The attachment object containing information about the audio/video file.
1651
- * @returns {Promise<Media>} A Promise that resolves to a Media object representing the processed audio/video attachment.
1864
+ * Asynchronously tests the creation of Discord client using the provided runtime.
1865
+ *
1866
+ * @param {IAgentRuntime} runtime - The agent runtime used to obtain the Discord service.
1867
+ * @returns {Promise<void>} - A Promise that resolves once the Discord client is ready.
1868
+ * @throws {Error} - If an error occurs while creating the Discord client.
1652
1869
  */
1653
- async processAudioVideoAttachment(attachment) {
1870
+ async testCreatingDiscordClient(runtime) {
1654
1871
  try {
1655
- const response = await fetch(attachment.url);
1656
- const audioVideoArrayBuffer = await response.arrayBuffer();
1657
- let audioBuffer;
1658
- if (attachment.contentType?.startsWith("audio/")) {
1659
- audioBuffer = Buffer.from(audioVideoArrayBuffer);
1660
- } else if (attachment.contentType?.startsWith("video/mp4")) {
1661
- audioBuffer = await this.extractAudioFromMP4(audioVideoArrayBuffer);
1872
+ this.discordClient = runtime.getService(
1873
+ ServiceTypes2.DISCORD
1874
+ );
1875
+ if (this.discordClient.client.isReady()) {
1876
+ logger4.success("DiscordService is already ready.");
1662
1877
  } else {
1663
- throw new Error("Unsupported audio/video format");
1878
+ logger4.info("Waiting for DiscordService to be ready...");
1879
+ await new Promise((resolve, reject) => {
1880
+ this.discordClient.client.once(Events.ClientReady, resolve);
1881
+ this.discordClient.client.once(Events.Error, reject);
1882
+ });
1664
1883
  }
1665
- const transcription = await this.runtime.useModel(
1666
- ModelTypes6.TRANSCRIPTION,
1667
- audioBuffer
1668
- );
1669
- const { title, description } = await generateSummary(
1670
- this.runtime,
1671
- transcription
1672
- );
1673
- return {
1674
- id: attachment.id,
1675
- url: attachment.url,
1676
- title: title || "Audio/Video Attachment",
1677
- source: attachment.contentType?.startsWith("audio/") ? "Audio" : "Video",
1678
- description: description || "User-uploaded audio/video attachment which has been transcribed",
1679
- text: transcription || "Audio/video content not available"
1680
- };
1681
1884
  } catch (error) {
1682
- console.error(
1683
- `Error processing audio/video attachment: ${error.message}`
1684
- );
1685
- return {
1686
- id: attachment.id,
1687
- url: attachment.url,
1688
- title: "Audio/Video Attachment",
1689
- source: attachment.contentType?.startsWith("audio/") ? "Audio" : "Video",
1690
- description: "An audio/video attachment (transcription failed)",
1691
- text: `This is an audio/video attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
1692
- };
1885
+ throw new Error(`Error in test creating Discord client: ${error}`);
1693
1886
  }
1694
1887
  }
1695
1888
  /**
1696
- * Extracts the audio stream from the provided MP4 data and converts it to MP3 format.
1889
+ * Asynchronously tests the join voice slash command functionality.
1697
1890
  *
1698
- * @param {ArrayBuffer} mp4Data - The MP4 data to extract audio from
1699
- * @returns {Promise<Buffer>} - A Promise that resolves with the converted audio data as a Buffer
1891
+ * @param {IAgentRuntime} runtime - The runtime environment for the agent.
1892
+ * @returns {Promise<void>} - A promise that resolves once the test is complete.
1893
+ * @throws {Error} - If there is an error in executing the slash command test.
1700
1894
  */
1701
- async extractAudioFromMP4(mp4Data) {
1702
- const tempMP4File = `temp_${Date.now()}.mp4`;
1703
- const tempAudioFile = `temp_${Date.now()}.mp3`;
1895
+ async testJoinVoiceSlashCommand(runtime) {
1704
1896
  try {
1705
- fs3.writeFileSync(tempMP4File, Buffer.from(mp4Data));
1706
- await new Promise((resolve, reject) => {
1707
- ffmpeg(tempMP4File).outputOptions("-vn").audioCodec("libmp3lame").save(tempAudioFile).on("end", () => {
1708
- resolve();
1709
- }).on("error", (err) => {
1710
- reject(err);
1711
- }).run();
1712
- });
1713
- const audioData = fs3.readFileSync(tempAudioFile);
1714
- return audioData;
1715
- } finally {
1716
- if (fs3.existsSync(tempMP4File)) {
1717
- fs3.unlinkSync(tempMP4File);
1718
- }
1719
- if (fs3.existsSync(tempAudioFile)) {
1720
- fs3.unlinkSync(tempAudioFile);
1897
+ await this.waitForVoiceManagerReady(this.discordClient);
1898
+ const channel = await this.getTestChannel(runtime);
1899
+ if (!channel || !channel.isTextBased()) {
1900
+ throw new Error("Invalid test channel for slash command test.");
1721
1901
  }
1902
+ const fakeJoinInteraction = {
1903
+ isCommand: () => true,
1904
+ commandName: "joinchannel",
1905
+ options: {
1906
+ get: (name) => name === "channel" ? { value: channel.id } : null
1907
+ },
1908
+ guild: channel.guild,
1909
+ deferReply: async () => {
1910
+ },
1911
+ editReply: async (message) => {
1912
+ logger4.info(`JoinChannel Slash Command Response: ${message}`);
1913
+ }
1914
+ };
1915
+ await this.discordClient.voiceManager.handleJoinChannelCommand(
1916
+ fakeJoinInteraction
1917
+ );
1918
+ logger4.success("Slash command test completed successfully.");
1919
+ } catch (error) {
1920
+ throw new Error(`Error in slash commands test: ${error}`);
1722
1921
  }
1723
1922
  }
1724
1923
  /**
1725
- * Processes a PDF attachment by fetching the PDF file from the specified URL,
1726
- * converting it to text, generating a summary, and returning a Media object
1727
- * with the extracted information.
1728
- * If an error occurs during processing, a placeholder Media object is returned
1729
- * with an error message.
1924
+ * Asynchronously tests the leave voice channel slash command.
1730
1925
  *
1731
- * @param {Attachment} attachment - The PDF attachment to process.
1732
- * @returns {Promise<Media>} A promise that resolves to a Media object representing
1733
- * the processed PDF attachment.
1926
+ * @param {IAgentRuntime} runtime - The Agent Runtime instance.
1927
+ * @returns {Promise<void>} A promise that resolves when the test is complete.
1734
1928
  */
1735
- async processPdfAttachment(attachment) {
1929
+ async testLeaveVoiceSlashCommand(runtime) {
1736
1930
  try {
1737
- const response = await fetch(attachment.url);
1738
- const pdfBuffer = await response.arrayBuffer();
1739
- const text = await this.runtime.getService(ServiceTypes3.PDF).convertPdfToText(Buffer.from(pdfBuffer));
1740
- const { title, description } = await generateSummary(this.runtime, text);
1741
- return {
1742
- id: attachment.id,
1743
- url: attachment.url,
1744
- title: title || "PDF Attachment",
1745
- source: "PDF",
1746
- description: description || "A PDF document",
1747
- text
1931
+ await this.waitForVoiceManagerReady(this.discordClient);
1932
+ const channel = await this.getTestChannel(runtime);
1933
+ if (!channel || !channel.isTextBased()) {
1934
+ throw new Error("Invalid test channel for slash command test.");
1935
+ }
1936
+ const fakeLeaveInteraction = {
1937
+ isCommand: () => true,
1938
+ commandName: "leavechannel",
1939
+ guildId: channel.guildId,
1940
+ reply: async (message) => {
1941
+ logger4.info(`LeaveChannel Slash Command Response: ${message}`);
1942
+ }
1748
1943
  };
1944
+ await this.discordClient.voiceManager.handleLeaveChannelCommand(
1945
+ fakeLeaveInteraction
1946
+ );
1947
+ logger4.success("Slash command test completed successfully.");
1749
1948
  } catch (error) {
1750
- console.error(`Error processing PDF attachment: ${error.message}`);
1751
- return {
1752
- id: attachment.id,
1753
- url: attachment.url,
1754
- title: "PDF Attachment (conversion failed)",
1755
- source: "PDF",
1756
- description: "A PDF document that could not be converted to text",
1757
- text: `This is a PDF attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes`
1758
- };
1949
+ throw new Error(`Error in slash commands test: ${error}`);
1759
1950
  }
1760
1951
  }
1761
1952
  /**
1762
- * Processes a plaintext attachment by fetching its content, generating a summary, and returning a Media object.
1763
- * @param {Attachment} attachment - The attachment object to process.
1764
- * @returns {Promise<Media>} A promise that resolves to a Media object representing the processed plaintext attachment.
1953
+ * Test Text to Speech playback.
1954
+ * @param {IAgentRuntime} runtime - The Agent Runtime instance.
1955
+ * @throws {Error} - If voice channel is invalid, voice connection fails to become ready, or no text to speech service found.
1765
1956
  */
1766
- async processPlaintextAttachment(attachment) {
1957
+ async testTextToSpeechPlayback(runtime) {
1767
1958
  try {
1768
- const response = await fetch(attachment.url);
1769
- const text = await response.text();
1770
- const { title, description } = await generateSummary(this.runtime, text);
1771
- return {
1772
- id: attachment.id,
1773
- url: attachment.url,
1774
- title: title || "Plaintext Attachment",
1775
- source: "Plaintext",
1776
- description: description || "A plaintext document",
1777
- text
1778
- };
1959
+ await this.waitForVoiceManagerReady(this.discordClient);
1960
+ const channel = await this.getTestChannel(runtime);
1961
+ if (!channel || channel.type !== ChannelType7.GuildVoice) {
1962
+ throw new Error("Invalid voice channel.");
1963
+ }
1964
+ await this.discordClient.voiceManager.joinChannel(channel);
1965
+ const guild = await this.getActiveGuild(this.discordClient);
1966
+ const guildId = guild.id;
1967
+ const connection = this.discordClient.voiceManager.getVoiceConnection(guildId);
1968
+ try {
1969
+ await entersState(connection, VoiceConnectionStatus.Ready, 1e4);
1970
+ logger4.success(`Voice connection is ready in guild: ${guildId}`);
1971
+ } catch (error) {
1972
+ throw new Error(`Voice connection failed to become ready: ${error}`);
1973
+ }
1974
+ let responseStream = null;
1975
+ try {
1976
+ responseStream = await runtime.useModel(
1977
+ ModelTypes7.TEXT_TO_SPEECH,
1978
+ `Hi! I'm ${runtime.character.name}! How are you doing today?`
1979
+ );
1980
+ } catch (_error) {
1981
+ throw new Error("No text to speech service found");
1982
+ }
1983
+ if (!responseStream) {
1984
+ throw new Error("TTS response stream is null or undefined.");
1985
+ }
1986
+ await this.playAudioStream(responseStream, connection);
1779
1987
  } catch (error) {
1780
- console.error(`Error processing plaintext attachment: ${error.message}`);
1781
- return {
1782
- id: attachment.id,
1783
- url: attachment.url,
1784
- title: "Plaintext Attachment (retrieval failed)",
1785
- source: "Plaintext",
1786
- description: "A plaintext document that could not be retrieved",
1787
- text: `This is a plaintext attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes`
1788
- };
1988
+ throw new Error(`Error in TTS playback test: ${error}`);
1789
1989
  }
1790
1990
  }
1791
1991
  /**
1792
- * Process the image attachment by fetching description and title using the IMAGE_DESCRIPTION model.
1793
- * If successful, returns a Media object populated with the details. If unsuccessful, creates a fallback
1794
- * Media object and logs the error.
1992
+ * Asynchronously tests sending a text message to a specified channel.
1795
1993
  *
1796
- * @param {Attachment} attachment - The attachment object containing the image details.
1797
- * @returns {Promise<Media>} A promise that resolves to a Media object.
1994
+ * @param {IAgentRuntime} runtime - The runtime for the agent.
1995
+ * @returns {Promise<void>} A Promise that resolves when the message is sent successfully.
1996
+ * @throws {Error} If there is an error in sending the text message.
1798
1997
  */
1799
- async processImageAttachment(attachment) {
1998
+ async testSendingTextMessage(runtime) {
1800
1999
  try {
1801
- const { description, title } = await this.runtime.useModel(
1802
- ModelTypes6.IMAGE_DESCRIPTION,
1803
- attachment.url
2000
+ const channel = await this.getTestChannel(runtime);
2001
+ await this.sendMessageToChannel(
2002
+ channel,
2003
+ "Testing Message",
2004
+ [TEST_IMAGE_URL]
1804
2005
  );
1805
- return {
1806
- id: attachment.id,
1807
- url: attachment.url,
1808
- title: title || "Image Attachment",
1809
- source: "Image",
1810
- description: description || "An image attachment",
1811
- text: description || "Image content not available"
1812
- };
1813
2006
  } catch (error) {
1814
- console.error(`Error processing image attachment: ${error.message}`);
1815
- return this.createFallbackImageMedia(attachment);
2007
+ throw new Error(`Error in sending text message: ${error}`);
1816
2008
  }
1817
2009
  }
1818
2010
  /**
1819
- * Creates a fallback Media object for image attachments that could not be recognized.
2011
+ * Asynchronously handles sending a test message using the given runtime and mock user data.
1820
2012
  *
1821
- * @param {Attachment} attachment - The attachment object containing image details.
1822
- * @returns {Media} - The fallback Media object with basic information about the image attachment.
1823
- */
1824
- createFallbackImageMedia(attachment) {
1825
- return {
1826
- id: attachment.id,
1827
- url: attachment.url,
1828
- title: "Image Attachment",
1829
- source: "Image",
1830
- description: "An image attachment (recognition failed)",
1831
- text: `This is an image attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
1832
- };
1833
- }
1834
- /**
1835
- * Process a video attachment to extract video information.
1836
- * @param {Attachment} attachment - The attachment object containing video information.
1837
- * @returns {Promise<Media>} A promise that resolves to a Media object with video details.
1838
- * @throws {Error} If video service is not available.
2013
+ * @param {IAgentRuntime} runtime - The agent runtime object.
2014
+ * @returns {Promise<void>} A Promise that resolves once the message is handled.
1839
2015
  */
1840
- async processVideoAttachment(attachment) {
1841
- const videoService = this.runtime.getService(
1842
- ServiceTypes3.VIDEO
1843
- );
1844
- if (!videoService) {
1845
- throw new Error("Video service not found");
1846
- }
1847
- if (videoService.isVideoUrl(attachment.url)) {
1848
- const videoInfo = await videoService.processVideo(
1849
- attachment.url,
1850
- this.runtime
1851
- );
1852
- return {
1853
- id: attachment.id,
1854
- url: attachment.url,
1855
- title: videoInfo.title,
1856
- source: "YouTube",
1857
- description: videoInfo.description,
1858
- text: videoInfo.text
2016
+ async testHandlingMessage(runtime) {
2017
+ try {
2018
+ const channel = await this.getTestChannel(runtime);
2019
+ const fakeMessage = {
2020
+ content: `Hello, ${runtime.character.name}! How are you?`,
2021
+ author: {
2022
+ id: "mock-user-id",
2023
+ username: "MockUser",
2024
+ bot: false
2025
+ },
2026
+ channel,
2027
+ id: "mock-message-id",
2028
+ createdTimestamp: Date.now(),
2029
+ mentions: {
2030
+ has: () => false
2031
+ },
2032
+ reference: null,
2033
+ attachments: []
1859
2034
  };
2035
+ await this.discordClient.messageManager.handleMessage(fakeMessage);
2036
+ } catch (error) {
2037
+ throw new Error(`Error in sending text message: ${error}`);
1860
2038
  }
1861
- return {
1862
- id: attachment.id,
1863
- url: attachment.url,
1864
- title: "Video Attachment",
1865
- source: "Video",
1866
- description: "A video attachment",
1867
- text: "Video content not available"
1868
- };
1869
2039
  }
2040
+ // #############################
2041
+ // Utility Functions
2042
+ // #############################
1870
2043
  /**
1871
- * Process a generic attachment and return a Media object with specified properties.
1872
- * @param {Attachment} attachment - The attachment object to process.
1873
- * @returns {Promise<Media>} A Promise that resolves to a Media object with specified properties.
2044
+ * Asynchronously retrieves the test channel associated with the provided runtime.
2045
+ *
2046
+ * @param {IAgentRuntime} runtime - The runtime object containing necessary information.
2047
+ * @returns {Promise<Channel>} The test channel retrieved from the Discord client.
2048
+ * @throws {Error} If no test channel is found.
1874
2049
  */
1875
- async processGenericAttachment(attachment) {
1876
- return {
1877
- id: attachment.id,
1878
- url: attachment.url,
1879
- title: "Generic Attachment",
1880
- source: "Generic",
1881
- description: "A generic attachment",
1882
- text: "Attachment content not available"
1883
- };
1884
- }
1885
- };
1886
-
1887
- // src/utils.ts
1888
- import {
1889
- ModelTypes as ModelTypes7,
1890
- logger as logger3,
1891
- parseJSONObjectFromText as parseJSONObjectFromText6,
1892
- trimTokens as trimTokens4
1893
- } from "@elizaos/core";
1894
- import {
1895
- ChannelType as ChannelType4,
1896
- PermissionsBitField,
1897
- ThreadChannel
1898
- } from "discord.js";
1899
- function getWavHeader(audioLength, sampleRate, channelCount = 1, bitsPerSample = 16) {
1900
- const wavHeader = Buffer.alloc(44);
1901
- wavHeader.write("RIFF", 0);
1902
- wavHeader.writeUInt32LE(36 + audioLength, 4);
1903
- wavHeader.write("WAVE", 8);
1904
- wavHeader.write("fmt ", 12);
1905
- wavHeader.writeUInt32LE(16, 16);
1906
- wavHeader.writeUInt16LE(1, 20);
1907
- wavHeader.writeUInt16LE(channelCount, 22);
1908
- wavHeader.writeUInt32LE(sampleRate, 24);
1909
- wavHeader.writeUInt32LE(sampleRate * bitsPerSample * channelCount / 8, 28);
1910
- wavHeader.writeUInt16LE(bitsPerSample * channelCount / 8, 32);
1911
- wavHeader.writeUInt16LE(bitsPerSample, 34);
1912
- wavHeader.write("data", 36);
1913
- wavHeader.writeUInt32LE(audioLength, 40);
1914
- return wavHeader;
1915
- }
1916
- var MAX_MESSAGE_LENGTH = 1900;
1917
- async function sendMessageInChunks(channel, content, _inReplyTo, files) {
1918
- const sentMessages = [];
1919
- const messages = splitMessage(content);
1920
- try {
1921
- for (let i = 0; i < messages.length; i++) {
1922
- const message = messages[i];
1923
- if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0) {
1924
- const options = {
1925
- content: message.trim()
1926
- };
1927
- if (i === messages.length - 1 && files && files.length > 0) {
1928
- options.files = files;
1929
- }
1930
- const m = await channel.send(options);
1931
- sentMessages.push(m);
1932
- }
1933
- }
1934
- } catch (error) {
1935
- logger3.error("Error sending message:", error);
1936
- }
1937
- return sentMessages;
1938
- }
1939
- function splitMessage(content) {
1940
- const messages = [];
1941
- let currentMessage = "";
1942
- const rawLines = content?.split("\n") || [];
1943
- const lines = rawLines.flatMap((line) => {
1944
- const chunks = [];
1945
- while (line.length > MAX_MESSAGE_LENGTH) {
1946
- chunks.push(line.slice(0, MAX_MESSAGE_LENGTH));
1947
- line = line.slice(MAX_MESSAGE_LENGTH);
1948
- }
1949
- chunks.push(line);
1950
- return chunks;
1951
- });
1952
- for (const line of lines) {
1953
- if (currentMessage.length + line.length + 1 > MAX_MESSAGE_LENGTH) {
1954
- messages.push(currentMessage.trim());
1955
- currentMessage = "";
1956
- }
1957
- currentMessage += `${line}
1958
- `;
1959
- }
1960
- if (currentMessage.trim().length > 0) {
1961
- messages.push(currentMessage.trim());
1962
- }
1963
- return messages;
1964
- }
1965
- function canSendMessage(channel) {
1966
- if (!channel) {
1967
- return {
1968
- canSend: false,
1969
- reason: "No channel given"
1970
- };
1971
- }
1972
- if (channel.type === ChannelType4.DM) {
1973
- return {
1974
- canSend: true,
1975
- reason: null
1976
- };
1977
- }
1978
- const botMember = channel.guild?.members.cache.get(channel.client.user.id);
1979
- if (!botMember) {
1980
- return {
1981
- canSend: false,
1982
- reason: "Not a guild channel or bot member not found"
1983
- };
1984
- }
1985
- const requiredPermissions = [
1986
- PermissionsBitField.Flags.ViewChannel,
1987
- PermissionsBitField.Flags.SendMessages,
1988
- PermissionsBitField.Flags.ReadMessageHistory
1989
- ];
1990
- if (channel instanceof ThreadChannel) {
1991
- requiredPermissions.push(PermissionsBitField.Flags.SendMessagesInThreads);
1992
- }
1993
- const permissions = channel.permissionsFor(botMember);
1994
- if (!permissions) {
1995
- return {
1996
- canSend: false,
1997
- reason: "Could not retrieve permissions"
1998
- };
1999
- }
2000
- const missingPermissions = requiredPermissions.filter(
2001
- (perm) => !permissions.has(perm)
2002
- );
2003
- return {
2004
- canSend: missingPermissions.length === 0,
2005
- missingPermissions,
2006
- reason: missingPermissions.length > 0 ? `Missing permissions: ${missingPermissions.map((p) => String(p)).join(", ")}` : null
2007
- };
2008
- }
2009
-
2010
- // src/messages.ts
2011
- var MessageManager = class {
2012
- client;
2013
- runtime;
2014
- attachmentManager;
2015
- getChannelType;
2016
- /**
2017
- * Constructor for a new instance of MyClass.
2018
- * @param {any} discordClient - The Discord client object.
2019
- */
2020
- constructor(discordClient) {
2021
- this.client = discordClient.client;
2022
- this.runtime = discordClient.runtime;
2023
- this.attachmentManager = new AttachmentManager(this.runtime);
2024
- this.getChannelType = discordClient.getChannelType;
2050
+ async getTestChannel(runtime) {
2051
+ const channelId = this.validateChannelId(runtime);
2052
+ const channel = await this.discordClient.client.channels.fetch(channelId);
2053
+ if (!channel) throw new Error("no test channel found!");
2054
+ return channel;
2025
2055
  }
2026
2056
  /**
2027
- * Handles incoming Discord messages and processes them accordingly.
2057
+ * Async function to send a message to a text-based channel.
2028
2058
  *
2029
- * @param {DiscordMessage} message - The Discord message to be handled
2059
+ * @param {TextChannel} channel - The text-based channel the message is being sent to.
2060
+ * @param {string} messageContent - The content of the message being sent.
2061
+ * @param {any[]} files - An array of files to include in the message.
2062
+ * @throws {Error} If the channel is not a text-based channel or does not exist.
2063
+ * @throws {Error} If there is an error sending the message.
2030
2064
  */
2031
- async handleMessage(message) {
2032
- if (this.runtime.character.settings?.discord?.allowedChannelIds && !this.runtime.character.settings.discord.allowedChannelIds.some(
2033
- (id) => id === message.channel.id
2034
- )) {
2035
- return;
2036
- }
2037
- if (message.interaction || message.author.id === this.client.user?.id) {
2038
- return;
2039
- }
2040
- if (this.runtime.character.settings?.discord?.shouldIgnoreBotMessages && message.author?.bot) {
2041
- return;
2042
- }
2043
- if (this.runtime.character.settings?.discord?.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType2.DM) {
2044
- return;
2045
- }
2046
- const entityId = createUniqueUuid4(this.runtime, message.author.id);
2047
- const userName = message.author.bot ? `${message.author.username}#${message.author.discriminator}` : message.author.username;
2048
- const name = message.author.displayName;
2049
- const channelId = message.channel.id;
2050
- const roomId = createUniqueUuid4(this.runtime, channelId);
2051
- let type;
2052
- let serverId;
2053
- if (message.guild) {
2054
- const guild = await message.guild.fetch();
2055
- type = await this.getChannelType(message.channel);
2056
- serverId = guild.id;
2057
- } else {
2058
- type = ChannelType5.DM;
2059
- serverId = void 0;
2060
- }
2061
- await this.runtime.ensureConnection({
2062
- entityId,
2063
- roomId,
2064
- userName,
2065
- name,
2066
- source: "discord",
2067
- channelId: message.channel.id,
2068
- serverId,
2069
- type
2070
- });
2065
+ async sendMessageToChannel(channel, messageContent, files) {
2071
2066
  try {
2072
- const canSendResult = canSendMessage(message.channel);
2073
- if (!canSendResult.canSend) {
2074
- return logger4.warn(
2075
- `Cannot send message to channel ${message.channel}`,
2076
- canSendResult
2067
+ if (!channel || !channel.isTextBased()) {
2068
+ throw new Error(
2069
+ "Channel is not a text-based channel or does not exist."
2077
2070
  );
2078
2071
  }
2079
- const { processedContent, attachments } = await this.processMessage(message);
2080
- const audioAttachments = message.attachments.filter(
2081
- (attachment) => attachment.contentType?.startsWith("audio/")
2072
+ await sendMessageInChunks(
2073
+ channel,
2074
+ messageContent,
2075
+ null,
2076
+ files
2082
2077
  );
2083
- if (audioAttachments.size > 0) {
2084
- const processedAudioAttachments = await this.attachmentManager.processAttachments(audioAttachments);
2085
- attachments.push(...processedAudioAttachments);
2086
- }
2087
- if (!processedContent && !attachments?.length) {
2088
- return;
2089
- }
2090
- const entityId2 = createUniqueUuid4(this.runtime, message.author.id);
2091
- const messageId = createUniqueUuid4(this.runtime, message.id);
2092
- const newMessage = {
2093
- id: messageId,
2094
- entityId: entityId2,
2095
- agentId: this.runtime.agentId,
2096
- roomId,
2097
- content: {
2098
- // name: name,
2099
- // userName: userName,
2100
- text: processedContent || " ",
2101
- attachments,
2102
- source: "discord",
2103
- url: message.url,
2104
- inReplyTo: message.reference?.messageId ? createUniqueUuid4(this.runtime, message.reference?.messageId) : void 0
2105
- },
2106
- createdAt: message.createdTimestamp
2107
- };
2108
- const callback = async (content, files) => {
2109
- try {
2110
- if (message.id && !content.inReplyTo) {
2111
- content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
2112
- }
2113
- const messages = await sendMessageInChunks(
2114
- message.channel,
2115
- content.text,
2116
- message.id,
2117
- files
2118
- );
2119
- const memories = [];
2120
- for (const m of messages) {
2121
- const actions = content.actions;
2122
- const memory = {
2123
- id: createUniqueUuid4(this.runtime, m.id),
2124
- entityId: this.runtime.agentId,
2125
- agentId: this.runtime.agentId,
2126
- content: {
2127
- ...content,
2128
- actions,
2129
- inReplyTo: messageId,
2130
- url: m.url,
2131
- channelType: type
2132
- },
2133
- roomId,
2134
- createdAt: m.createdTimestamp
2135
- };
2136
- memories.push(memory);
2137
- }
2138
- for (const m of memories) {
2139
- await this.runtime.getMemoryManager("messages").createMemory(m);
2140
- }
2141
- return memories;
2142
- } catch (error) {
2143
- console.error("Error sending message:", error);
2144
- return [];
2145
- }
2146
- };
2147
- this.runtime.emitEvent(["DISCORD_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, EventTypes.MESSAGE_RECEIVED], {
2148
- runtime: this.runtime,
2149
- message: newMessage,
2150
- callback
2151
- });
2152
2078
  } catch (error) {
2153
- console.error("Error handling message:", error);
2079
+ throw new Error(`Error sending message: ${error}`);
2154
2080
  }
2155
2081
  }
2156
2082
  /**
2157
- * Processes the message content, mentions, code blocks, attachments, and URLs to generate
2158
- * processed content and media attachments.
2083
+ * Play an audio stream from a given response stream using the provided VoiceConnection.
2159
2084
  *
2160
- * @param {DiscordMessage} message The message to process
2161
- * @returns {Promise<{ processedContent: string; attachments: Media[] }>} Processed content and media attachments
2085
+ * @param {any} responseStream - The response stream to play as audio.
2086
+ * @param {VoiceConnection} connection - The VoiceConnection to use for playing the audio.
2087
+ * @returns {Promise<void>} - A Promise that resolves when the TTS playback is finished.
2162
2088
  */
2163
- async processMessage(message) {
2164
- let processedContent = message.content;
2165
- let attachments = [];
2166
- const mentionRegex = /<@!?(\d+)>/g;
2167
- processedContent = processedContent.replace(
2168
- mentionRegex,
2169
- (match2, entityId) => {
2170
- const user = message.mentions.users.get(entityId);
2171
- if (user) {
2172
- return `${user.username} (@${entityId})`;
2173
- }
2174
- return match2;
2175
- }
2176
- );
2177
- const codeBlockRegex = /```([\s\S]*?)```/g;
2178
- let match;
2179
- while (match = codeBlockRegex.exec(processedContent)) {
2180
- const codeBlock = match[1];
2181
- const lines = codeBlock.split("\n");
2182
- const title = lines[0];
2183
- const description = lines.slice(0, 3).join("\n");
2184
- const attachmentId = `code-${Date.now()}-${Math.floor(
2185
- Math.random() * 1e3
2186
- )}`.slice(-5);
2187
- attachments.push({
2188
- id: attachmentId,
2189
- url: "",
2190
- title: title || "Code Block",
2191
- source: "Code",
2192
- description,
2193
- text: codeBlock
2194
- });
2195
- processedContent = processedContent.replace(
2196
- match[0],
2197
- `Code Block (${attachmentId})`
2198
- );
2199
- }
2200
- if (message.attachments.size > 0) {
2201
- attachments = await this.attachmentManager.processAttachments(
2202
- message.attachments
2203
- );
2204
- }
2205
- const urlRegex = /(https?:\/\/[^\s]+)/g;
2206
- const urls = processedContent.match(urlRegex) || [];
2207
- for (const url of urls) {
2208
- if (this.runtime.getService(ServiceTypes4.VIDEO)?.isVideoUrl(url)) {
2209
- const videoService = this.runtime.getService(
2210
- ServiceTypes4.VIDEO
2211
- );
2212
- if (!videoService) {
2213
- throw new Error("Video service not found");
2214
- }
2215
- const videoInfo = await videoService.processVideo(url, this.runtime);
2216
- attachments.push({
2217
- id: `youtube-${Date.now()}`,
2218
- url,
2219
- title: videoInfo.title,
2220
- source: "YouTube",
2221
- description: videoInfo.description,
2222
- text: videoInfo.text
2223
- });
2224
- } else {
2225
- const browserService = this.runtime.getService(
2226
- ServiceTypes4.BROWSER
2227
- );
2228
- if (!browserService) {
2229
- throw new Error("Browser service not found");
2230
- }
2231
- const { title, description: summary } = await browserService.getPageContent(url, this.runtime);
2232
- attachments.push({
2233
- id: `webpage-${Date.now()}`,
2234
- url,
2235
- title: title || "Web Page",
2236
- source: "Web",
2237
- description: summary,
2238
- text: summary
2239
- });
2089
+ async playAudioStream(responseStream, connection) {
2090
+ const audioPlayer = createAudioPlayer({
2091
+ behaviors: {
2092
+ noSubscriber: NoSubscriberBehavior.Pause
2240
2093
  }
2241
- }
2242
- return { processedContent, attachments };
2094
+ });
2095
+ const audioResource = createAudioResource(responseStream);
2096
+ audioPlayer.play(audioResource);
2097
+ connection.subscribe(audioPlayer);
2098
+ logger4.success("TTS playback started successfully.");
2099
+ await new Promise((resolve, reject) => {
2100
+ audioPlayer.once(AudioPlayerStatus.Idle, () => {
2101
+ logger4.info("TTS playback finished.");
2102
+ resolve();
2103
+ });
2104
+ audioPlayer.once("error", (error) => {
2105
+ reject(error);
2106
+ throw new Error(`TTS playback error: ${error}`);
2107
+ });
2108
+ });
2243
2109
  }
2244
2110
  /**
2245
- * Asynchronously fetches the bot's username and discriminator from Discord API.
2111
+ * Retrieves the active guild where the bot is currently connected to a voice channel.
2246
2112
  *
2247
- * @param {string} botToken The token of the bot to authenticate the request
2248
- * @returns {Promise<string>} A promise that resolves with the bot's username and discriminator
2249
- * @throws {Error} If there is an error while fetching the bot details
2113
+ * @param {DiscordService} discordClient The DiscordService instance used to interact with the Discord API.
2114
+ * @returns {Promise<Guild>} The active guild where the bot is currently connected to a voice channel.
2115
+ * @throws {Error} If no active voice connection is found for the bot.
2250
2116
  */
2251
- async fetchBotName(botToken) {
2252
- const url = "https://discord.com/api/v10/users/@me";
2253
- const response = await fetch(url, {
2254
- method: "GET",
2255
- headers: {
2256
- Authorization: `Bot ${botToken}`
2257
- }
2258
- });
2259
- if (!response.ok) {
2260
- throw new Error(`Error fetching bot details: ${response.statusText}`);
2117
+ async getActiveGuild(discordClient) {
2118
+ const guilds = await discordClient.client.guilds.fetch();
2119
+ const fullGuilds = await Promise.all(guilds.map((guild) => guild.fetch()));
2120
+ const activeGuild = fullGuilds.find((g) => g.members.me?.voice.channelId);
2121
+ if (!activeGuild) {
2122
+ throw new Error("No active voice connection found for the bot.");
2261
2123
  }
2262
- const data = await response.json();
2263
- const discriminator = data.discriminator;
2264
- return data.username + (discriminator ? `#${discriminator}` : "");
2124
+ return activeGuild;
2265
2125
  }
2266
- };
2267
-
2268
- // src/providers/channelState.ts
2269
- import { ChannelType as ChannelType6 } from "@elizaos/core";
2270
- var channelStateProvider = {
2271
- name: "channelState",
2272
- get: async (runtime, message, state) => {
2273
- const room = state.data?.room ?? await runtime.getRoom(message.roomId);
2274
- if (!room) {
2275
- throw new Error("No room found");
2126
+ /**
2127
+ * Waits for the VoiceManager in the Discord client to be ready.
2128
+ *
2129
+ * @param {DiscordService} discordClient - The Discord client to check for VoiceManager readiness.
2130
+ * @throws {Error} If the Discord client is not initialized.
2131
+ * @returns {Promise<void>} A promise that resolves when the VoiceManager is ready.
2132
+ */
2133
+ async waitForVoiceManagerReady(discordClient) {
2134
+ if (!discordClient) {
2135
+ throw new Error("Discord client is not initialized.");
2276
2136
  }
2277
- if (message.content.source !== "discord") {
2278
- return {
2279
- data: null,
2280
- values: {},
2281
- text: ""
2282
- };
2137
+ if (!discordClient.voiceManager.isReady()) {
2138
+ await new Promise((resolve, reject) => {
2139
+ discordClient.voiceManager.once("ready", resolve);
2140
+ discordClient.voiceManager.once("error", reject);
2141
+ });
2283
2142
  }
2284
- const agentName = state?.agentName || "The agent";
2285
- const senderName = state?.senderName || "someone";
2286
- let responseText = "";
2287
- let channelType = "";
2288
- let serverName = "";
2289
- let channelId = "";
2290
- const serverId = room.serverId;
2291
- if (room.type === ChannelType6.DM) {
2292
- channelType = "DM";
2293
- 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.`;
2294
- } else {
2295
- channelType = "GROUP";
2296
- if (!serverId) {
2297
- console.error("No server ID found");
2298
- return {
2299
- data: {
2300
- room,
2301
- channelType
2302
- },
2303
- values: {
2304
- channelType
2305
- },
2306
- text: ""
2307
- };
2308
- }
2309
- channelId = room.channelId;
2310
- const discordService = runtime.getService(
2311
- ServiceTypes2.DISCORD
2143
+ }
2144
+ /**
2145
+ * Validates the Discord test channel ID by checking if it is set in the runtime or environment variables.
2146
+ * If the test channel ID is not set, an error is thrown.
2147
+ *
2148
+ * @param {IAgentRuntime} runtime The runtime object containing the settings and environment variables.
2149
+ * @returns {string} The validated Discord test channel ID.
2150
+ */
2151
+ validateChannelId(runtime) {
2152
+ const testChannelId = runtime.getSetting("DISCORD_TEST_CHANNEL_ID") || process.env.DISCORD_TEST_CHANNEL_ID;
2153
+ if (!testChannelId) {
2154
+ throw new Error(
2155
+ "DISCORD_TEST_CHANNEL_ID is not set. Please provide a valid channel ID in the environment variables."
2312
2156
  );
2313
- if (!discordService) {
2314
- console.warn("No discord client found");
2315
- return {
2316
- data: {
2317
- room,
2318
- channelType,
2319
- serverId
2320
- },
2321
- values: {
2322
- channelType,
2323
- serverId
2324
- },
2325
- text: ""
2326
- };
2327
- }
2328
- const guild = discordService.client.guilds.cache.get(serverId);
2329
- serverName = guild.name;
2330
- responseText = `${agentName} is currently having a conversation in the channel \`@${channelId} in the server \`${serverName}\` (@${serverId})`;
2331
- responseText += `
2332
- ${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.`;
2333
2157
  }
2334
- return {
2335
- data: {
2336
- room,
2337
- channelType,
2338
- serverId,
2339
- serverName,
2340
- channelId
2341
- },
2342
- values: {
2343
- channelType,
2344
- serverName,
2345
- channelId
2346
- },
2347
- text: responseText
2348
- };
2158
+ return testChannelId;
2349
2159
  }
2350
2160
  };
2351
- var channelState_default = channelStateProvider;
2352
2161
 
2353
- // src/providers/voiceState.ts
2354
- import { getVoiceConnection } from "@discordjs/voice";
2355
- import { ChannelType as ChannelType7 } from "@elizaos/core";
2356
- var voiceStateProvider = {
2357
- name: "voiceState",
2358
- get: async (runtime, message, state) => {
2359
- const room = await runtime.getRoom(message.roomId);
2360
- if (!room) {
2361
- throw new Error("No room found");
2362
- }
2363
- if (room.type !== ChannelType7.GROUP) {
2364
- return {
2365
- data: {
2366
- isInVoiceChannel: false,
2367
- room
2368
- },
2369
- values: {
2370
- isInVoiceChannel: "false",
2371
- roomType: room.type
2372
- },
2373
- text: ""
2374
- };
2375
- }
2376
- const serverId = room.serverId;
2377
- if (!serverId) {
2378
- throw new Error("No server ID found 10");
2379
- }
2380
- const connection = getVoiceConnection(serverId);
2381
- const agentName = state?.agentName || "The agent";
2382
- if (!connection) {
2383
- return {
2384
- data: {
2385
- isInVoiceChannel: false,
2386
- room,
2387
- serverId
2388
- },
2389
- values: {
2390
- isInVoiceChannel: "false",
2391
- serverId
2392
- },
2393
- text: `${agentName} is not currently in a voice channel`
2394
- };
2395
- }
2396
- const worldId = room.worldId;
2397
- const world = await runtime.getWorld(worldId);
2398
- if (!world) {
2399
- throw new Error("No world found");
2400
- }
2401
- const worldName = world.name;
2402
- const roomType = room.type;
2403
- const channelId = room.channelId;
2404
- const channelName = room.name;
2405
- if (!channelId) {
2406
- return {
2407
- data: {
2408
- isInVoiceChannel: true,
2409
- room,
2410
- serverId,
2411
- world,
2412
- connection
2413
- },
2414
- values: {
2415
- isInVoiceChannel: "true",
2416
- serverId,
2417
- worldName,
2418
- roomType
2419
- },
2420
- text: `${agentName} is in an invalid voice channel`
2421
- };
2422
- }
2423
- return {
2424
- data: {
2425
- isInVoiceChannel: true,
2426
- room,
2427
- serverId,
2428
- world,
2429
- connection,
2430
- channelId,
2431
- channelName
2432
- },
2433
- values: {
2434
- isInVoiceChannel: "true",
2435
- serverId,
2436
- worldName,
2437
- roomType,
2438
- channelId,
2439
- channelName
2440
- },
2441
- text: `${agentName} is currently in the voice channel: ${channelName} (ID: ${channelId})`
2442
- };
2443
- }
2162
+ // src/service.ts
2163
+ import { ChannelType as ChannelType10, EventTypes as EventTypes2, Role, Service, createUniqueUuid as createUniqueUuid6, logger as logger7 } from "@elizaos/core";
2164
+ import { ChannelType as DiscordChannelType4, Client as DiscordJsClient, Events as Events2, GatewayIntentBits, Partials, PermissionsBitField as PermissionsBitField2 } from "discord.js";
2165
+
2166
+ // src/constants.ts
2167
+ var MESSAGE_CONSTANTS = {
2168
+ MAX_MESSAGES: 10,
2169
+ RECENT_MESSAGE_COUNT: 3,
2170
+ CHAT_HISTORY_COUNT: 5,
2171
+ INTEREST_DECAY_TIME: 5 * 60 * 1e3,
2172
+ // 5 minutes
2173
+ PARTIAL_INTEREST_DECAY: 3 * 60 * 1e3,
2174
+ // 3 minutes
2175
+ DEFAULT_SIMILARITY_THRESHOLD: 0.3,
2176
+ DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.2
2444
2177
  };
2445
- var voiceState_default = voiceStateProvider;
2178
+ var DISCORD_SERVICE_NAME = "discord";
2446
2179
 
2447
- // src/tests.ts
2180
+ // src/messages.ts
2448
2181
  import {
2449
- AudioPlayerStatus,
2450
- NoSubscriberBehavior,
2451
- VoiceConnectionStatus,
2452
- createAudioPlayer,
2453
- createAudioResource,
2454
- entersState
2455
- } from "@discordjs/voice";
2182
+ ChannelType as ChannelType8,
2183
+ EventTypes,
2184
+ ServiceTypes as ServiceTypes4,
2185
+ createUniqueUuid as createUniqueUuid4,
2186
+ logger as logger5
2187
+ } from "@elizaos/core";
2188
+ import {
2189
+ ChannelType as DiscordChannelType2
2190
+ } from "discord.js";
2191
+
2192
+ // src/attachments.ts
2193
+ import fs3 from "node:fs";
2194
+ import { trimTokens as trimTokens4 } from "@elizaos/core";
2195
+ import { parseJSONObjectFromText as parseJSONObjectFromText6 } from "@elizaos/core";
2456
2196
  import {
2457
2197
  ModelTypes as ModelTypes8,
2458
- logger as logger5
2198
+ ServiceTypes as ServiceTypes3
2459
2199
  } from "@elizaos/core";
2460
- import { ChannelType as ChannelType8, Events } from "discord.js";
2461
- var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
2462
- var DiscordTestSuite = class {
2463
- name = "discord";
2464
- discordClient = null;
2465
- tests;
2200
+ import { Collection } from "discord.js";
2201
+ import ffmpeg from "fluent-ffmpeg";
2202
+ async function generateSummary(runtime, text) {
2203
+ text = await trimTokens4(text, 1e5, runtime);
2204
+ const prompt = `Please generate a concise summary for the following text:
2205
+
2206
+ Text: """
2207
+ ${text}
2208
+ """
2209
+
2210
+ Respond with a JSON object in the following format:
2211
+ \`\`\`json
2212
+ {
2213
+ "title": "Generated Title",
2214
+ "summary": "Generated summary and/or description of the text"
2215
+ }
2216
+ \`\`\``;
2217
+ const response = await runtime.useModel(ModelTypes8.TEXT_SMALL, {
2218
+ prompt
2219
+ });
2220
+ const parsedResponse = parseJSONObjectFromText6(response);
2221
+ if (parsedResponse?.title && parsedResponse?.summary) {
2222
+ return {
2223
+ title: parsedResponse.title,
2224
+ description: parsedResponse.summary
2225
+ };
2226
+ }
2227
+ return {
2228
+ title: "",
2229
+ description: ""
2230
+ };
2231
+ }
2232
+ var AttachmentManager = class {
2233
+ attachmentCache = /* @__PURE__ */ new Map();
2234
+ runtime;
2466
2235
  /**
2467
- * Constructor for initializing the tests array with test cases to be executed.
2236
+ * Constructor for creating a new instance of the class.
2468
2237
  *
2469
- * @constructor
2470
- * @this {TestSuite}
2238
+ * @param {IAgentRuntime} runtime The runtime object to be injected into the instance.
2471
2239
  */
2472
- constructor() {
2473
- this.tests = [
2474
- {
2475
- name: "Initialize Discord Client",
2476
- fn: this.testCreatingDiscordClient.bind(this)
2477
- },
2478
- {
2479
- name: "Slash Commands - Join Voice",
2480
- fn: this.testJoinVoiceSlashCommand.bind(this)
2481
- },
2482
- {
2483
- name: "Voice Playback & TTS",
2484
- fn: this.testTextToSpeechPlayback.bind(this)
2485
- },
2486
- {
2487
- name: "Send Message with Attachments",
2488
- fn: this.testSendingTextMessage.bind(this)
2489
- },
2490
- {
2491
- name: "Handle Incoming Messages",
2492
- fn: this.testHandlingMessage.bind(this)
2493
- },
2494
- {
2495
- name: "Slash Commands - Leave Voice",
2496
- fn: this.testLeaveVoiceSlashCommand.bind(this)
2240
+ constructor(runtime) {
2241
+ this.runtime = runtime;
2242
+ }
2243
+ /**
2244
+ * Processes attachments and returns an array of Media objects.
2245
+ * @param {Collection<string, Attachment> | Attachment[]} attachments - The attachments to be processed
2246
+ * @returns {Promise<Media[]>} - An array of processed Media objects
2247
+ */
2248
+ async processAttachments(attachments) {
2249
+ const processedAttachments = [];
2250
+ const attachmentCollection = attachments instanceof Collection ? attachments : new Collection(attachments.map((att) => [att.id, att]));
2251
+ for (const [, attachment] of attachmentCollection) {
2252
+ const media = await this.processAttachment(attachment);
2253
+ if (media) {
2254
+ processedAttachments.push(media);
2497
2255
  }
2498
- ];
2256
+ }
2257
+ return processedAttachments;
2499
2258
  }
2500
2259
  /**
2501
- * Asynchronously tests the creation of Discord client using the provided runtime.
2260
+ * Processes the provided attachment to generate a media object.
2261
+ * If the media for the attachment URL is already cached, it will return the cached media.
2262
+ * Otherwise, it will determine the type of attachment (PDF, text, audio, video, image, generic)
2263
+ * and call the corresponding processing method to generate the media object.
2502
2264
  *
2503
- * @param {IAgentRuntime} runtime - The agent runtime used to obtain the Discord service.
2504
- * @returns {Promise<void>} - A Promise that resolves once the Discord client is ready.
2505
- * @throws {Error} - If an error occurs while creating the Discord client.
2265
+ * @param attachment The attachment to process
2266
+ * @returns A promise that resolves to a Media object representing the attachment, or null if the attachment could not be processed
2506
2267
  */
2507
- async testCreatingDiscordClient(runtime) {
2268
+ async processAttachment(attachment) {
2269
+ if (this.attachmentCache.has(attachment.url)) {
2270
+ return this.attachmentCache.get(attachment.url);
2271
+ }
2272
+ let media = null;
2273
+ if (attachment.contentType?.startsWith("application/pdf")) {
2274
+ media = await this.processPdfAttachment(attachment);
2275
+ } else if (attachment.contentType?.startsWith("text/plain")) {
2276
+ media = await this.processPlaintextAttachment(attachment);
2277
+ } else if (attachment.contentType?.startsWith("audio/") || attachment.contentType?.startsWith("video/mp4")) {
2278
+ media = await this.processAudioVideoAttachment(attachment);
2279
+ } else if (attachment.contentType?.startsWith("image/")) {
2280
+ media = await this.processImageAttachment(attachment);
2281
+ } else if (attachment.contentType?.startsWith("video/") || this.runtime.getService(ServiceTypes3.VIDEO).isVideoUrl(attachment.url)) {
2282
+ media = await this.processVideoAttachment(attachment);
2283
+ } else {
2284
+ media = await this.processGenericAttachment(attachment);
2285
+ }
2286
+ if (media) {
2287
+ this.attachmentCache.set(attachment.url, media);
2288
+ }
2289
+ return media;
2290
+ }
2291
+ /**
2292
+ * Asynchronously processes an audio or video attachment provided as input and returns a Media object.
2293
+ * @param {Attachment} attachment - The attachment object containing information about the audio/video file.
2294
+ * @returns {Promise<Media>} A Promise that resolves to a Media object representing the processed audio/video attachment.
2295
+ */
2296
+ async processAudioVideoAttachment(attachment) {
2508
2297
  try {
2509
- this.discordClient = runtime.getService(
2510
- ServiceTypes2.DISCORD
2511
- );
2512
- if (this.discordClient.client.isReady()) {
2513
- logger5.success("DiscordService is already ready.");
2298
+ const response = await fetch(attachment.url);
2299
+ const audioVideoArrayBuffer = await response.arrayBuffer();
2300
+ let audioBuffer;
2301
+ if (attachment.contentType?.startsWith("audio/")) {
2302
+ audioBuffer = Buffer.from(audioVideoArrayBuffer);
2303
+ } else if (attachment.contentType?.startsWith("video/mp4")) {
2304
+ audioBuffer = await this.extractAudioFromMP4(audioVideoArrayBuffer);
2514
2305
  } else {
2515
- logger5.info("Waiting for DiscordService to be ready...");
2516
- await new Promise((resolve, reject) => {
2517
- this.discordClient.client.once(Events.ClientReady, resolve);
2518
- this.discordClient.client.once(Events.Error, reject);
2519
- });
2306
+ throw new Error("Unsupported audio/video format");
2520
2307
  }
2308
+ const transcription = await this.runtime.useModel(
2309
+ ModelTypes8.TRANSCRIPTION,
2310
+ audioBuffer
2311
+ );
2312
+ const { title, description } = await generateSummary(
2313
+ this.runtime,
2314
+ transcription
2315
+ );
2316
+ return {
2317
+ id: attachment.id,
2318
+ url: attachment.url,
2319
+ title: title || "Audio/Video Attachment",
2320
+ source: attachment.contentType?.startsWith("audio/") ? "Audio" : "Video",
2321
+ description: description || "User-uploaded audio/video attachment which has been transcribed",
2322
+ text: transcription || "Audio/video content not available"
2323
+ };
2521
2324
  } catch (error) {
2522
- throw new Error(`Error in test creating Discord client: ${error}`);
2325
+ console.error(
2326
+ `Error processing audio/video attachment: ${error.message}`
2327
+ );
2328
+ return {
2329
+ id: attachment.id,
2330
+ url: attachment.url,
2331
+ title: "Audio/Video Attachment",
2332
+ source: attachment.contentType?.startsWith("audio/") ? "Audio" : "Video",
2333
+ description: "An audio/video attachment (transcription failed)",
2334
+ text: `This is an audio/video attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
2335
+ };
2523
2336
  }
2524
2337
  }
2525
2338
  /**
2526
- * Asynchronously tests the join voice slash command functionality.
2339
+ * Extracts the audio stream from the provided MP4 data and converts it to MP3 format.
2527
2340
  *
2528
- * @param {IAgentRuntime} runtime - The runtime environment for the agent.
2529
- * @returns {Promise<void>} - A promise that resolves once the test is complete.
2530
- * @throws {Error} - If there is an error in executing the slash command test.
2341
+ * @param {ArrayBuffer} mp4Data - The MP4 data to extract audio from
2342
+ * @returns {Promise<Buffer>} - A Promise that resolves with the converted audio data as a Buffer
2531
2343
  */
2532
- async testJoinVoiceSlashCommand(runtime) {
2344
+ async extractAudioFromMP4(mp4Data) {
2345
+ const tempMP4File = `temp_${Date.now()}.mp4`;
2346
+ const tempAudioFile = `temp_${Date.now()}.mp3`;
2533
2347
  try {
2534
- await this.waitForVoiceManagerReady(this.discordClient);
2535
- const channel = await this.getTestChannel(runtime);
2536
- if (!channel || !channel.isTextBased()) {
2537
- throw new Error("Invalid test channel for slash command test.");
2348
+ fs3.writeFileSync(tempMP4File, Buffer.from(mp4Data));
2349
+ await new Promise((resolve, reject) => {
2350
+ ffmpeg(tempMP4File).outputOptions("-vn").audioCodec("libmp3lame").save(tempAudioFile).on("end", () => {
2351
+ resolve();
2352
+ }).on("error", (err) => {
2353
+ reject(err);
2354
+ }).run();
2355
+ });
2356
+ const audioData = fs3.readFileSync(tempAudioFile);
2357
+ return audioData;
2358
+ } finally {
2359
+ if (fs3.existsSync(tempMP4File)) {
2360
+ fs3.unlinkSync(tempMP4File);
2361
+ }
2362
+ if (fs3.existsSync(tempAudioFile)) {
2363
+ fs3.unlinkSync(tempAudioFile);
2538
2364
  }
2539
- const fakeJoinInteraction = {
2540
- isCommand: () => true,
2541
- commandName: "joinchannel",
2542
- options: {
2543
- get: (name) => name === "channel" ? { value: channel.id } : null
2544
- },
2545
- guild: channel.guild,
2546
- deferReply: async () => {
2547
- },
2548
- editReply: async (message) => {
2549
- logger5.info(`JoinChannel Slash Command Response: ${message}`);
2550
- }
2551
- };
2552
- await this.discordClient.voiceManager.handleJoinChannelCommand(
2553
- fakeJoinInteraction
2554
- );
2555
- logger5.success("Slash command test completed successfully.");
2556
- } catch (error) {
2557
- throw new Error(`Error in slash commands test: ${error}`);
2558
2365
  }
2559
2366
  }
2560
2367
  /**
2561
- * Asynchronously tests the leave voice channel slash command.
2368
+ * Processes a PDF attachment by fetching the PDF file from the specified URL,
2369
+ * converting it to text, generating a summary, and returning a Media object
2370
+ * with the extracted information.
2371
+ * If an error occurs during processing, a placeholder Media object is returned
2372
+ * with an error message.
2562
2373
  *
2563
- * @param {IAgentRuntime} runtime - The Agent Runtime instance.
2564
- * @returns {Promise<void>} A promise that resolves when the test is complete.
2374
+ * @param {Attachment} attachment - The PDF attachment to process.
2375
+ * @returns {Promise<Media>} A promise that resolves to a Media object representing
2376
+ * the processed PDF attachment.
2565
2377
  */
2566
- async testLeaveVoiceSlashCommand(runtime) {
2378
+ async processPdfAttachment(attachment) {
2567
2379
  try {
2568
- await this.waitForVoiceManagerReady(this.discordClient);
2569
- const channel = await this.getTestChannel(runtime);
2570
- if (!channel || !channel.isTextBased()) {
2571
- throw new Error("Invalid test channel for slash command test.");
2572
- }
2573
- const fakeLeaveInteraction = {
2574
- isCommand: () => true,
2575
- commandName: "leavechannel",
2576
- guildId: channel.guildId,
2577
- reply: async (message) => {
2578
- logger5.info(`LeaveChannel Slash Command Response: ${message}`);
2579
- }
2380
+ const response = await fetch(attachment.url);
2381
+ const pdfBuffer = await response.arrayBuffer();
2382
+ const text = await this.runtime.getService(ServiceTypes3.PDF).convertPdfToText(Buffer.from(pdfBuffer));
2383
+ const { title, description } = await generateSummary(this.runtime, text);
2384
+ return {
2385
+ id: attachment.id,
2386
+ url: attachment.url,
2387
+ title: title || "PDF Attachment",
2388
+ source: "PDF",
2389
+ description: description || "A PDF document",
2390
+ text
2580
2391
  };
2581
- await this.discordClient.voiceManager.handleLeaveChannelCommand(
2582
- fakeLeaveInteraction
2583
- );
2584
- logger5.success("Slash command test completed successfully.");
2585
2392
  } catch (error) {
2586
- throw new Error(`Error in slash commands test: ${error}`);
2393
+ console.error(`Error processing PDF attachment: ${error.message}`);
2394
+ return {
2395
+ id: attachment.id,
2396
+ url: attachment.url,
2397
+ title: "PDF Attachment (conversion failed)",
2398
+ source: "PDF",
2399
+ description: "A PDF document that could not be converted to text",
2400
+ text: `This is a PDF attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes`
2401
+ };
2587
2402
  }
2588
2403
  }
2589
2404
  /**
2590
- * Test Text to Speech playback.
2591
- * @param {IAgentRuntime} runtime - The Agent Runtime instance.
2592
- * @throws {Error} - If voice channel is invalid, voice connection fails to become ready, or no text to speech service found.
2405
+ * Processes a plaintext attachment by fetching its content, generating a summary, and returning a Media object.
2406
+ * @param {Attachment} attachment - The attachment object to process.
2407
+ * @returns {Promise<Media>} A promise that resolves to a Media object representing the processed plaintext attachment.
2593
2408
  */
2594
- async testTextToSpeechPlayback(runtime) {
2409
+ async processPlaintextAttachment(attachment) {
2595
2410
  try {
2596
- await this.waitForVoiceManagerReady(this.discordClient);
2597
- const channel = await this.getTestChannel(runtime);
2598
- if (!channel || channel.type !== ChannelType8.GuildVoice) {
2599
- throw new Error("Invalid voice channel.");
2600
- }
2601
- await this.discordClient.voiceManager.joinChannel(channel);
2602
- const guild = await this.getActiveGuild(this.discordClient);
2603
- const guildId = guild.id;
2604
- const connection = this.discordClient.voiceManager.getVoiceConnection(guildId);
2605
- try {
2606
- await entersState(connection, VoiceConnectionStatus.Ready, 1e4);
2607
- logger5.success(`Voice connection is ready in guild: ${guildId}`);
2608
- } catch (error) {
2609
- throw new Error(`Voice connection failed to become ready: ${error}`);
2610
- }
2611
- let responseStream = null;
2612
- try {
2613
- responseStream = await runtime.useModel(
2614
- ModelTypes8.TEXT_TO_SPEECH,
2615
- `Hi! I'm ${runtime.character.name}! How are you doing today?`
2616
- );
2617
- } catch (_error) {
2618
- throw new Error("No text to speech service found");
2619
- }
2620
- if (!responseStream) {
2621
- throw new Error("TTS response stream is null or undefined.");
2622
- }
2623
- await this.playAudioStream(responseStream, connection);
2411
+ const response = await fetch(attachment.url);
2412
+ const text = await response.text();
2413
+ const { title, description } = await generateSummary(this.runtime, text);
2414
+ return {
2415
+ id: attachment.id,
2416
+ url: attachment.url,
2417
+ title: title || "Plaintext Attachment",
2418
+ source: "Plaintext",
2419
+ description: description || "A plaintext document",
2420
+ text
2421
+ };
2624
2422
  } catch (error) {
2625
- throw new Error(`Error in TTS playback test: ${error}`);
2423
+ console.error(`Error processing plaintext attachment: ${error.message}`);
2424
+ return {
2425
+ id: attachment.id,
2426
+ url: attachment.url,
2427
+ title: "Plaintext Attachment (retrieval failed)",
2428
+ source: "Plaintext",
2429
+ description: "A plaintext document that could not be retrieved",
2430
+ text: `This is a plaintext attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes`
2431
+ };
2626
2432
  }
2627
2433
  }
2628
2434
  /**
2629
- * Asynchronously tests sending a text message to a specified channel.
2435
+ * Process the image attachment by fetching description and title using the IMAGE_DESCRIPTION model.
2436
+ * If successful, returns a Media object populated with the details. If unsuccessful, creates a fallback
2437
+ * Media object and logs the error.
2630
2438
  *
2631
- * @param {IAgentRuntime} runtime - The runtime for the agent.
2632
- * @returns {Promise<void>} A Promise that resolves when the message is sent successfully.
2633
- * @throws {Error} If there is an error in sending the text message.
2439
+ * @param {Attachment} attachment - The attachment object containing the image details.
2440
+ * @returns {Promise<Media>} A promise that resolves to a Media object.
2634
2441
  */
2635
- async testSendingTextMessage(runtime) {
2442
+ async processImageAttachment(attachment) {
2636
2443
  try {
2637
- const channel = await this.getTestChannel(runtime);
2638
- await this.sendMessageToChannel(
2639
- channel,
2640
- "Testing Message",
2641
- [TEST_IMAGE_URL]
2444
+ const { description, title } = await this.runtime.useModel(
2445
+ ModelTypes8.IMAGE_DESCRIPTION,
2446
+ attachment.url
2642
2447
  );
2448
+ return {
2449
+ id: attachment.id,
2450
+ url: attachment.url,
2451
+ title: title || "Image Attachment",
2452
+ source: "Image",
2453
+ description: description || "An image attachment",
2454
+ text: description || "Image content not available"
2455
+ };
2643
2456
  } catch (error) {
2644
- throw new Error(`Error in sending text message: ${error}`);
2457
+ console.error(`Error processing image attachment: ${error.message}`);
2458
+ return this.createFallbackImageMedia(attachment);
2645
2459
  }
2646
2460
  }
2647
2461
  /**
2648
- * Asynchronously handles sending a test message using the given runtime and mock user data.
2462
+ * Creates a fallback Media object for image attachments that could not be recognized.
2649
2463
  *
2650
- * @param {IAgentRuntime} runtime - The agent runtime object.
2651
- * @returns {Promise<void>} A Promise that resolves once the message is handled.
2464
+ * @param {Attachment} attachment - The attachment object containing image details.
2465
+ * @returns {Media} - The fallback Media object with basic information about the image attachment.
2652
2466
  */
2653
- async testHandlingMessage(runtime) {
2654
- try {
2655
- const channel = await this.getTestChannel(runtime);
2656
- const fakeMessage = {
2657
- content: `Hello, ${runtime.character.name}! How are you?`,
2658
- author: {
2659
- id: "mock-user-id",
2660
- username: "MockUser",
2661
- bot: false
2662
- },
2663
- channel,
2664
- id: "mock-message-id",
2665
- createdTimestamp: Date.now(),
2666
- mentions: {
2667
- has: () => false
2668
- },
2669
- reference: null,
2670
- attachments: []
2467
+ createFallbackImageMedia(attachment) {
2468
+ return {
2469
+ id: attachment.id,
2470
+ url: attachment.url,
2471
+ title: "Image Attachment",
2472
+ source: "Image",
2473
+ description: "An image attachment (recognition failed)",
2474
+ text: `This is an image attachment. File name: ${attachment.name}, Size: ${attachment.size} bytes, Content type: ${attachment.contentType}`
2475
+ };
2476
+ }
2477
+ /**
2478
+ * Process a video attachment to extract video information.
2479
+ * @param {Attachment} attachment - The attachment object containing video information.
2480
+ * @returns {Promise<Media>} A promise that resolves to a Media object with video details.
2481
+ * @throws {Error} If video service is not available.
2482
+ */
2483
+ async processVideoAttachment(attachment) {
2484
+ const videoService = this.runtime.getService(
2485
+ ServiceTypes3.VIDEO
2486
+ );
2487
+ if (!videoService) {
2488
+ throw new Error("Video service not found");
2489
+ }
2490
+ if (videoService.isVideoUrl(attachment.url)) {
2491
+ const videoInfo = await videoService.processVideo(
2492
+ attachment.url,
2493
+ this.runtime
2494
+ );
2495
+ return {
2496
+ id: attachment.id,
2497
+ url: attachment.url,
2498
+ title: videoInfo.title,
2499
+ source: "YouTube",
2500
+ description: videoInfo.description,
2501
+ text: videoInfo.text
2671
2502
  };
2672
- await this.discordClient.messageManager.handleMessage(fakeMessage);
2673
- } catch (error) {
2674
- throw new Error(`Error in sending text message: ${error}`);
2675
2503
  }
2504
+ return {
2505
+ id: attachment.id,
2506
+ url: attachment.url,
2507
+ title: "Video Attachment",
2508
+ source: "Video",
2509
+ description: "A video attachment",
2510
+ text: "Video content not available"
2511
+ };
2676
2512
  }
2677
- // #############################
2678
- // Utility Functions
2679
- // #############################
2680
2513
  /**
2681
- * Asynchronously retrieves the test channel associated with the provided runtime.
2682
- *
2683
- * @param {IAgentRuntime} runtime - The runtime object containing necessary information.
2684
- * @returns {Promise<Channel>} The test channel retrieved from the Discord client.
2685
- * @throws {Error} If no test channel is found.
2514
+ * Process a generic attachment and return a Media object with specified properties.
2515
+ * @param {Attachment} attachment - The attachment object to process.
2516
+ * @returns {Promise<Media>} A Promise that resolves to a Media object with specified properties.
2686
2517
  */
2687
- async getTestChannel(runtime) {
2688
- const channelId = this.validateChannelId(runtime);
2689
- const channel = await this.discordClient.client.channels.fetch(channelId);
2690
- if (!channel) throw new Error("no test channel found!");
2691
- return channel;
2518
+ async processGenericAttachment(attachment) {
2519
+ return {
2520
+ id: attachment.id,
2521
+ url: attachment.url,
2522
+ title: "Generic Attachment",
2523
+ source: "Generic",
2524
+ description: "A generic attachment",
2525
+ text: "Attachment content not available"
2526
+ };
2692
2527
  }
2528
+ };
2529
+
2530
+ // src/messages.ts
2531
+ var MessageManager = class {
2532
+ client;
2533
+ runtime;
2534
+ attachmentManager;
2535
+ getChannelType;
2693
2536
  /**
2694
- * Async function to send a message to a text-based channel.
2537
+ * Constructor for a new instance of MyClass.
2538
+ * @param {any} discordClient - The Discord client object.
2539
+ */
2540
+ constructor(discordClient) {
2541
+ this.client = discordClient.client;
2542
+ this.runtime = discordClient.runtime;
2543
+ this.attachmentManager = new AttachmentManager(this.runtime);
2544
+ this.getChannelType = discordClient.getChannelType;
2545
+ }
2546
+ /**
2547
+ * Handles incoming Discord messages and processes them accordingly.
2695
2548
  *
2696
- * @param {TextChannel} channel - The text-based channel the message is being sent to.
2697
- * @param {string} messageContent - The content of the message being sent.
2698
- * @param {any[]} files - An array of files to include in the message.
2699
- * @throws {Error} If the channel is not a text-based channel or does not exist.
2700
- * @throws {Error} If there is an error sending the message.
2549
+ * @param {DiscordMessage} message - The Discord message to be handled
2701
2550
  */
2702
- async sendMessageToChannel(channel, messageContent, files) {
2551
+ async handleMessage(message) {
2552
+ if (this.runtime.character.settings?.discord?.allowedChannelIds && !this.runtime.character.settings.discord.allowedChannelIds.some(
2553
+ (id) => id === message.channel.id
2554
+ )) {
2555
+ return;
2556
+ }
2557
+ if (message.interaction || message.author.id === this.client.user?.id) {
2558
+ return;
2559
+ }
2560
+ if (this.runtime.character.settings?.discord?.shouldIgnoreBotMessages && message.author?.bot) {
2561
+ return;
2562
+ }
2563
+ if (this.runtime.character.settings?.discord?.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType2.DM) {
2564
+ return;
2565
+ }
2566
+ const entityId = createUniqueUuid4(this.runtime, message.author.id);
2567
+ const userName = message.author.bot ? `${message.author.username}#${message.author.discriminator}` : message.author.username;
2568
+ const name = message.author.displayName;
2569
+ const channelId = message.channel.id;
2570
+ const roomId = createUniqueUuid4(this.runtime, channelId);
2571
+ let type;
2572
+ let serverId;
2573
+ if (message.guild) {
2574
+ const guild = await message.guild.fetch();
2575
+ type = await this.getChannelType(message.channel);
2576
+ serverId = guild.id;
2577
+ } else {
2578
+ type = ChannelType8.DM;
2579
+ serverId = void 0;
2580
+ }
2581
+ await this.runtime.ensureConnection({
2582
+ entityId,
2583
+ roomId,
2584
+ userName,
2585
+ name,
2586
+ source: "discord",
2587
+ channelId: message.channel.id,
2588
+ serverId,
2589
+ type
2590
+ });
2703
2591
  try {
2704
- if (!channel || !channel.isTextBased()) {
2705
- throw new Error(
2706
- "Channel is not a text-based channel or does not exist."
2592
+ const canSendResult = canSendMessage(message.channel);
2593
+ if (!canSendResult.canSend) {
2594
+ return logger5.warn(
2595
+ `Cannot send message to channel ${message.channel}`,
2596
+ canSendResult
2707
2597
  );
2708
2598
  }
2709
- await sendMessageInChunks(
2710
- channel,
2711
- messageContent,
2712
- null,
2713
- files
2599
+ const { processedContent, attachments } = await this.processMessage(message);
2600
+ const audioAttachments = message.attachments.filter(
2601
+ (attachment) => attachment.contentType?.startsWith("audio/")
2714
2602
  );
2603
+ if (audioAttachments.size > 0) {
2604
+ const processedAudioAttachments = await this.attachmentManager.processAttachments(audioAttachments);
2605
+ attachments.push(...processedAudioAttachments);
2606
+ }
2607
+ if (!processedContent && !attachments?.length) {
2608
+ return;
2609
+ }
2610
+ const entityId2 = createUniqueUuid4(this.runtime, message.author.id);
2611
+ const messageId = createUniqueUuid4(this.runtime, message.id);
2612
+ const newMessage = {
2613
+ id: messageId,
2614
+ entityId: entityId2,
2615
+ agentId: this.runtime.agentId,
2616
+ roomId,
2617
+ content: {
2618
+ // name: name,
2619
+ // userName: userName,
2620
+ text: processedContent || " ",
2621
+ attachments,
2622
+ source: "discord",
2623
+ url: message.url,
2624
+ inReplyTo: message.reference?.messageId ? createUniqueUuid4(this.runtime, message.reference?.messageId) : void 0
2625
+ },
2626
+ createdAt: message.createdTimestamp
2627
+ };
2628
+ const callback = async (content, files) => {
2629
+ try {
2630
+ if (message.id && !content.inReplyTo) {
2631
+ content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
2632
+ }
2633
+ const messages = await sendMessageInChunks(
2634
+ message.channel,
2635
+ content.text,
2636
+ message.id,
2637
+ files
2638
+ );
2639
+ const memories = [];
2640
+ for (const m of messages) {
2641
+ const actions = content.actions;
2642
+ const memory = {
2643
+ id: createUniqueUuid4(this.runtime, m.id),
2644
+ entityId: this.runtime.agentId,
2645
+ agentId: this.runtime.agentId,
2646
+ content: {
2647
+ ...content,
2648
+ actions,
2649
+ inReplyTo: messageId,
2650
+ url: m.url,
2651
+ channelType: type
2652
+ },
2653
+ roomId,
2654
+ createdAt: m.createdTimestamp
2655
+ };
2656
+ memories.push(memory);
2657
+ }
2658
+ for (const m of memories) {
2659
+ await this.runtime.getMemoryManager("messages").createMemory(m);
2660
+ }
2661
+ return memories;
2662
+ } catch (error) {
2663
+ console.error("Error sending message:", error);
2664
+ return [];
2665
+ }
2666
+ };
2667
+ this.runtime.emitEvent(["DISCORD_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, EventTypes.MESSAGE_RECEIVED], {
2668
+ runtime: this.runtime,
2669
+ message: newMessage,
2670
+ callback
2671
+ });
2715
2672
  } catch (error) {
2716
- throw new Error(`Error sending message: ${error}`);
2673
+ console.error("Error handling message:", error);
2717
2674
  }
2718
2675
  }
2719
2676
  /**
2720
- * Play an audio stream from a given response stream using the provided VoiceConnection.
2677
+ * Processes the message content, mentions, code blocks, attachments, and URLs to generate
2678
+ * processed content and media attachments.
2721
2679
  *
2722
- * @param {any} responseStream - The response stream to play as audio.
2723
- * @param {VoiceConnection} connection - The VoiceConnection to use for playing the audio.
2724
- * @returns {Promise<void>} - A Promise that resolves when the TTS playback is finished.
2680
+ * @param {DiscordMessage} message The message to process
2681
+ * @returns {Promise<{ processedContent: string; attachments: Media[] }>} Processed content and media attachments
2725
2682
  */
2726
- async playAudioStream(responseStream, connection) {
2727
- const audioPlayer = createAudioPlayer({
2728
- behaviors: {
2729
- noSubscriber: NoSubscriberBehavior.Pause
2683
+ async processMessage(message) {
2684
+ let processedContent = message.content;
2685
+ let attachments = [];
2686
+ const mentionRegex = /<@!?(\d+)>/g;
2687
+ processedContent = processedContent.replace(
2688
+ mentionRegex,
2689
+ (match2, entityId) => {
2690
+ const user = message.mentions.users.get(entityId);
2691
+ if (user) {
2692
+ return `${user.username} (@${entityId})`;
2693
+ }
2694
+ return match2;
2730
2695
  }
2731
- });
2732
- const audioResource = createAudioResource(responseStream);
2733
- audioPlayer.play(audioResource);
2734
- connection.subscribe(audioPlayer);
2735
- logger5.success("TTS playback started successfully.");
2736
- await new Promise((resolve, reject) => {
2737
- audioPlayer.once(AudioPlayerStatus.Idle, () => {
2738
- logger5.info("TTS playback finished.");
2739
- resolve();
2740
- });
2741
- audioPlayer.once("error", (error) => {
2742
- reject(error);
2743
- throw new Error(`TTS playback error: ${error}`);
2696
+ );
2697
+ const codeBlockRegex = /```([\s\S]*?)```/g;
2698
+ let match;
2699
+ while (match = codeBlockRegex.exec(processedContent)) {
2700
+ const codeBlock = match[1];
2701
+ const lines = codeBlock.split("\n");
2702
+ const title = lines[0];
2703
+ const description = lines.slice(0, 3).join("\n");
2704
+ const attachmentId = `code-${Date.now()}-${Math.floor(
2705
+ Math.random() * 1e3
2706
+ )}`.slice(-5);
2707
+ attachments.push({
2708
+ id: attachmentId,
2709
+ url: "",
2710
+ title: title || "Code Block",
2711
+ source: "Code",
2712
+ description,
2713
+ text: codeBlock
2744
2714
  });
2745
- });
2746
- }
2747
- /**
2748
- * Retrieves the active guild where the bot is currently connected to a voice channel.
2749
- *
2750
- * @param {DiscordService} discordClient The DiscordService instance used to interact with the Discord API.
2751
- * @returns {Promise<Guild>} The active guild where the bot is currently connected to a voice channel.
2752
- * @throws {Error} If no active voice connection is found for the bot.
2753
- */
2754
- async getActiveGuild(discordClient) {
2755
- const guilds = await discordClient.client.guilds.fetch();
2756
- const fullGuilds = await Promise.all(guilds.map((guild) => guild.fetch()));
2757
- const activeGuild = fullGuilds.find((g) => g.members.me?.voice.channelId);
2758
- if (!activeGuild) {
2759
- throw new Error("No active voice connection found for the bot.");
2715
+ processedContent = processedContent.replace(
2716
+ match[0],
2717
+ `Code Block (${attachmentId})`
2718
+ );
2760
2719
  }
2761
- return activeGuild;
2762
- }
2763
- /**
2764
- * Waits for the VoiceManager in the Discord client to be ready.
2765
- *
2766
- * @param {DiscordService} discordClient - The Discord client to check for VoiceManager readiness.
2767
- * @throws {Error} If the Discord client is not initialized.
2768
- * @returns {Promise<void>} A promise that resolves when the VoiceManager is ready.
2769
- */
2770
- async waitForVoiceManagerReady(discordClient) {
2771
- if (!discordClient) {
2772
- throw new Error("Discord client is not initialized.");
2720
+ if (message.attachments.size > 0) {
2721
+ attachments = await this.attachmentManager.processAttachments(
2722
+ message.attachments
2723
+ );
2773
2724
  }
2774
- if (!discordClient.voiceManager.isReady()) {
2775
- await new Promise((resolve, reject) => {
2776
- discordClient.voiceManager.once("ready", resolve);
2777
- discordClient.voiceManager.once("error", reject);
2778
- });
2725
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
2726
+ const urls = processedContent.match(urlRegex) || [];
2727
+ for (const url of urls) {
2728
+ if (this.runtime.getService(ServiceTypes4.VIDEO)?.isVideoUrl(url)) {
2729
+ const videoService = this.runtime.getService(
2730
+ ServiceTypes4.VIDEO
2731
+ );
2732
+ if (!videoService) {
2733
+ throw new Error("Video service not found");
2734
+ }
2735
+ const videoInfo = await videoService.processVideo(url, this.runtime);
2736
+ attachments.push({
2737
+ id: `youtube-${Date.now()}`,
2738
+ url,
2739
+ title: videoInfo.title,
2740
+ source: "YouTube",
2741
+ description: videoInfo.description,
2742
+ text: videoInfo.text
2743
+ });
2744
+ } else {
2745
+ const browserService = this.runtime.getService(
2746
+ ServiceTypes4.BROWSER
2747
+ );
2748
+ if (!browserService) {
2749
+ throw new Error("Browser service not found");
2750
+ }
2751
+ const { title, description: summary } = await browserService.getPageContent(url, this.runtime);
2752
+ attachments.push({
2753
+ id: `webpage-${Date.now()}`,
2754
+ url,
2755
+ title: title || "Web Page",
2756
+ source: "Web",
2757
+ description: summary,
2758
+ text: summary
2759
+ });
2760
+ }
2779
2761
  }
2762
+ return { processedContent, attachments };
2780
2763
  }
2781
2764
  /**
2782
- * Validates the Discord test channel ID by checking if it is set in the runtime or environment variables.
2783
- * If the test channel ID is not set, an error is thrown.
2765
+ * Asynchronously fetches the bot's username and discriminator from Discord API.
2784
2766
  *
2785
- * @param {IAgentRuntime} runtime The runtime object containing the settings and environment variables.
2786
- * @returns {string} The validated Discord test channel ID.
2767
+ * @param {string} botToken The token of the bot to authenticate the request
2768
+ * @returns {Promise<string>} A promise that resolves with the bot's username and discriminator
2769
+ * @throws {Error} If there is an error while fetching the bot details
2787
2770
  */
2788
- validateChannelId(runtime) {
2789
- const testChannelId = runtime.getSetting("DISCORD_TEST_CHANNEL_ID") || process.env.DISCORD_TEST_CHANNEL_ID;
2790
- if (!testChannelId) {
2791
- throw new Error(
2792
- "DISCORD_TEST_CHANNEL_ID is not set. Please provide a valid channel ID in the environment variables."
2793
- );
2771
+ async fetchBotName(botToken) {
2772
+ const url = "https://discord.com/api/v10/users/@me";
2773
+ const response = await fetch(url, {
2774
+ method: "GET",
2775
+ headers: {
2776
+ Authorization: `Bot ${botToken}`
2777
+ }
2778
+ });
2779
+ if (!response.ok) {
2780
+ throw new Error(`Error fetching bot details: ${response.statusText}`);
2794
2781
  }
2795
- return testChannelId;
2782
+ const data = await response.json();
2783
+ const discriminator = data.discriminator;
2784
+ return data.username + (discriminator ? `#${discriminator}` : "");
2796
2785
  }
2797
2786
  };
2798
2787
 
@@ -3512,7 +3501,7 @@ var VoiceManager = class extends EventEmitter {
3512
3501
  console.log(`Joining channel: ${chosenChannel.name}`);
3513
3502
  await this.joinChannel(chosenChannel);
3514
3503
  } else {
3515
- console.warn("No suitable voice channel found to join.");
3504
+ logger6.debug("Warning: No suitable voice channel found to join.");
3516
3505
  }
3517
3506
  } catch (error) {
3518
3507
  console.error("Error selecting or joining a voice channel:", error);
@@ -3626,7 +3615,7 @@ var VoiceManager = class extends EventEmitter {
3626
3615
  }
3627
3616
  };
3628
3617
 
3629
- // src/index.ts
3618
+ // src/service.ts
3630
3619
  var DiscordService = class _DiscordService extends Service {
3631
3620
  static serviceType = DISCORD_SERVICE_NAME;
3632
3621
  capabilityDescription = "The agent is able to send and receive messages on discord";
@@ -4437,27 +4426,29 @@ var DiscordService = class _DiscordService extends Service {
4437
4426
  this.client?.emit("voiceManagerReady");
4438
4427
  }
4439
4428
  };
4429
+
4430
+ // src/index.ts
4440
4431
  var discordPlugin = {
4441
4432
  name: "discord",
4442
4433
  description: "Discord client plugin",
4443
4434
  services: [DiscordService],
4444
4435
  actions: [
4445
4436
  chatWithAttachments_default,
4446
- downloadMedia_default,
4447
- voiceJoin_default,
4448
- voiceLeave_default,
4449
- summarizeConversation_default,
4450
- transcribeMedia_default
4437
+ downloadMedia,
4438
+ joinVoice,
4439
+ leaveVoice,
4440
+ summarize,
4441
+ transcribeMedia
4451
4442
  ],
4452
- providers: [channelState_default, voiceState_default],
4443
+ providers: [channelStateProvider, voiceStateProvider],
4453
4444
  tests: [new DiscordTestSuite()],
4454
4445
  init: async (config, runtime) => {
4455
4446
  const token = runtime.getSetting("DISCORD_API_TOKEN");
4456
4447
  if (!token || token.trim() === "") {
4457
- logger7.warn(
4448
+ logger8.warn(
4458
4449
  "Discord API Token not provided - Discord plugin is loaded but will not be functional"
4459
4450
  );
4460
- logger7.warn(
4451
+ logger8.warn(
4461
4452
  "To enable Discord functionality, please provide DISCORD_API_TOKEN in your .eliza/.env file"
4462
4453
  );
4463
4454
  }
@@ -4465,7 +4456,6 @@ var discordPlugin = {
4465
4456
  };
4466
4457
  var index_default = discordPlugin;
4467
4458
  export {
4468
- DiscordService,
4469
4459
  index_default as default
4470
4460
  };
4471
4461
  //# sourceMappingURL=index.js.map