@elizaos/plugin-discord 1.0.0-beta.7 → 1.0.3

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,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/index.ts
2
9
  import { logger as logger8 } from "@elizaos/core";
3
10
 
@@ -134,14 +141,21 @@ var chatWithAttachments = {
134
141
  return;
135
142
  }
136
143
  const { objective, attachmentIds } = attachmentData;
137
- const attachments = state.data.recentMessages.filter((msg) => msg.content.attachments && msg.content.attachments.length > 0).flatMap((msg) => msg.content.attachments).filter(
138
- (attachment) => attachmentIds.map((attch) => attch.toLowerCase().slice(0, 5)).includes(attachment.id.toLowerCase().slice(0, 5)) || // or check the other way
144
+ const conversationLength = runtime.getConversationLength();
145
+ const recentMessages = await runtime.getMemories({
146
+ tableName: "messages",
147
+ roomId: message.roomId,
148
+ count: conversationLength,
149
+ unique: false
150
+ });
151
+ const attachments = recentMessages.filter((msg) => msg.content.attachments && msg.content.attachments.length > 0).flatMap((msg) => msg.content.attachments).filter(
152
+ (attachment) => attachment && (attachmentIds.map((attch) => attch.toLowerCase().slice(0, 5)).includes(attachment.id.toLowerCase().slice(0, 5)) || // or check the other way
139
153
  attachmentIds.some((id) => {
140
154
  const attachmentId = id.toLowerCase().slice(0, 5);
141
- return attachment.id.toLowerCase().includes(attachmentId);
142
- })
155
+ return attachment && attachment.id.toLowerCase().includes(attachmentId);
156
+ }))
143
157
  );
144
- const attachmentsWithText = attachments.map((attachment) => `# ${attachment.title}
158
+ const attachmentsWithText = attachments.filter((attachment) => !!attachment).map((attachment) => `# ${attachment.title}
145
159
  ${attachment.text}`).join("\n\n");
146
160
  let currentSummary = "";
147
161
  const chunkSize = 8192;
@@ -328,6 +342,10 @@ var downloadMedia = {
328
342
  },
329
343
  handler: async (runtime, message, state, _options, callback) => {
330
344
  const videoService = runtime.getService(ServiceType.VIDEO);
345
+ if (!videoService) {
346
+ console.error("Video service not found");
347
+ return;
348
+ }
331
349
  const mediaUrl = await getMediaUrl(runtime, message, state);
332
350
  if (!mediaUrl) {
333
351
  console.error("Couldn't get media URL from messages");
@@ -850,7 +868,14 @@ var transcribeMedia = {
850
868
  );
851
869
  return;
852
870
  }
853
- const attachment = state.data.recentMessages.filter((msg) => msg.content.attachments && msg.content.attachments.length > 0).flatMap((msg) => msg.content.attachments).find((attachment2) => attachment2.id.toLowerCase() === attachmentId.toLowerCase());
871
+ const conversationLength = runtime.getConversationLength();
872
+ const recentMessages = await runtime.getMemories({
873
+ tableName: "messages",
874
+ roomId: message.roomId,
875
+ count: conversationLength,
876
+ unique: false
877
+ });
878
+ const attachment = recentMessages.filter((msg) => msg.content.attachments && msg.content.attachments.length > 0).flatMap((msg) => msg.content.attachments).find((attachment2) => attachment2?.id.toLowerCase() === attachmentId.toLowerCase());
854
879
  if (!attachment) {
855
880
  console.error(`Couldn't find attachment with ID ${attachmentId}`);
856
881
  await runtime.createMemory(
@@ -962,7 +987,7 @@ var joinVoice = {
962
987
  return false;
963
988
  }
964
989
  const room = state.data.room ?? await runtime.getRoom(message.roomId);
965
- if (room?.type !== ChannelType2.GROUP) {
990
+ if (room?.type !== ChannelType2.GROUP && room?.type !== ChannelType2.VOICE_GROUP) {
966
991
  return false;
967
992
  }
968
993
  const client = runtime.getService(ServiceType2.DISCORD);
@@ -975,10 +1000,11 @@ var joinVoice = {
975
1000
  description: "Join a voice channel to participate in voice chat.",
976
1001
  handler: async (runtime, message, state, _options, callback) => {
977
1002
  const room = state.data.room ?? await runtime.getRoom(message.roomId);
1003
+ const messageContent = message?.content?.text?.toLowerCase() ?? "";
978
1004
  if (!room) {
979
1005
  throw new Error("No room found");
980
1006
  }
981
- if (room.type !== ChannelType2.GROUP) {
1007
+ if (room?.type !== ChannelType2.GROUP && room?.type !== ChannelType2.VOICE_GROUP) {
982
1008
  return false;
983
1009
  }
984
1010
  const serverId = room.serverId;
@@ -997,7 +1023,6 @@ var joinVoice = {
997
1023
  );
998
1024
  const targetChannel = voiceChannels.find((channel) => {
999
1025
  const name = channel.name.toLowerCase();
1000
- const messageContent = message?.content?.text;
1001
1026
  const replacedName = name.replace(/[^a-z0-9 ]/g, "");
1002
1027
  return name.includes(messageContent) || messageContent.includes(name) || replacedName.includes(messageContent) || messageContent.includes(replacedName);
1003
1028
  });
@@ -1011,7 +1036,8 @@ var joinVoice = {
1011
1036
  (member2) => createUniqueUuid2(runtime, member2.id) === message.entityId
1012
1037
  );
1013
1038
  if (member?.voice?.channel) {
1014
- voiceManager.joinChannel(member?.voice?.channel);
1039
+ const userVoiceChannel = member.voice.channel;
1040
+ voiceManager.joinChannel(userVoiceChannel);
1015
1041
  await runtime.createMemory(
1016
1042
  {
1017
1043
  entityId: message.entityId,
@@ -1019,7 +1045,7 @@ var joinVoice = {
1019
1045
  roomId: message.roomId,
1020
1046
  content: {
1021
1047
  source: "discord",
1022
- thought: `I joined the voice channel ${member?.voice?.channel?.name}`,
1048
+ thought: `I joined the voice channel ${userVoiceChannel.name}`,
1023
1049
  actions: ["JOIN_VOICE_STARTED"]
1024
1050
  },
1025
1051
  metadata: {
@@ -1032,10 +1058,10 @@ var joinVoice = {
1032
1058
  {
1033
1059
  entityId: message.entityId,
1034
1060
  agentId: message.agentId,
1035
- roomId: createUniqueUuid2(runtime, targetChannel.id),
1061
+ roomId: createUniqueUuid2(runtime, userVoiceChannel.id),
1036
1062
  content: {
1037
1063
  source: "discord",
1038
- thought: `I joined the voice channel ${targetChannel.name}`,
1064
+ thought: `I joined the voice channel ${userVoiceChannel.name}`,
1039
1065
  actions: ["JOIN_VOICE_STARTED"]
1040
1066
  },
1041
1067
  metadata: {
@@ -1084,7 +1110,7 @@ You should only respond with the name of the voice channel or none, no commentar
1084
1110
  roomId: message.roomId,
1085
1111
  content: {
1086
1112
  source: "discord",
1087
- thought: `I joined the voice channel ${member?.voice?.channel?.name}`,
1113
+ thought: `I joined the voice channel ${targetChannel2.name}`,
1088
1114
  actions: ["JOIN_VOICE_STARTED"]
1089
1115
  },
1090
1116
  metadata: {
@@ -1269,7 +1295,7 @@ var leaveVoice = {
1269
1295
  return false;
1270
1296
  }
1271
1297
  const room = state.data.room ?? await runtime.getRoom(message.roomId);
1272
- if (room?.type !== ChannelType3.GROUP) {
1298
+ if (room?.type !== ChannelType3.GROUP && room?.type !== ChannelType3.VOICE_GROUP) {
1273
1299
  return false;
1274
1300
  }
1275
1301
  const isConnectedToVoice = service.client.voice.adapters.size > 0;
@@ -1281,7 +1307,7 @@ var leaveVoice = {
1281
1307
  if (!room) {
1282
1308
  throw new Error("No room found");
1283
1309
  }
1284
- if (room.type !== ChannelType3.GROUP) {
1310
+ if (room?.type !== ChannelType3.GROUP && room?.type !== ChannelType3.VOICE_GROUP) {
1285
1311
  throw new Error("Not a group");
1286
1312
  }
1287
1313
  const serverId = room.serverId;
@@ -1517,7 +1543,7 @@ var channelStateProvider = {
1517
1543
  }
1518
1544
  if (message.content.source !== "discord") {
1519
1545
  return {
1520
- data: null,
1546
+ data: {},
1521
1547
  values: {},
1522
1548
  text: ""
1523
1549
  };
@@ -1564,7 +1590,24 @@ var channelStateProvider = {
1564
1590
  text: ""
1565
1591
  };
1566
1592
  }
1567
- const guild = discordService.client.guilds.cache.get(serverId);
1593
+ const guild = discordService.client?.guilds.cache.get(serverId);
1594
+ if (!guild) {
1595
+ console.warn(`Guild not found for serverId: ${serverId}`);
1596
+ return {
1597
+ data: {
1598
+ room,
1599
+ channelType,
1600
+ serverId,
1601
+ channelId
1602
+ },
1603
+ values: {
1604
+ channelType,
1605
+ serverId,
1606
+ channelId
1607
+ },
1608
+ text: ""
1609
+ };
1610
+ }
1568
1611
  serverName = guild.name;
1569
1612
  responseText = `${agentName} is currently having a conversation in the channel \`@${channelId} in the server \`${serverName}\` (@${serverId})`;
1570
1613
  responseText += `
@@ -1729,10 +1772,7 @@ import {
1729
1772
  import fs3 from "node:fs";
1730
1773
  import { trimTokens as trimTokens3 } from "@elizaos/core";
1731
1774
  import { parseJSONObjectFromText as parseJSONObjectFromText5 } from "@elizaos/core";
1732
- import {
1733
- ModelType as ModelType6,
1734
- ServiceType as ServiceType3
1735
- } from "@elizaos/core";
1775
+ import { ModelType as ModelType6, ServiceType as ServiceType3 } from "@elizaos/core";
1736
1776
  import { Collection } from "discord.js";
1737
1777
  import ffmpeg from "fluent-ffmpeg";
1738
1778
  async function generateSummary(runtime, text) {
@@ -1814,7 +1854,7 @@ var AttachmentManager = class {
1814
1854
  media = await this.processAudioVideoAttachment(attachment);
1815
1855
  } else if (attachment.contentType?.startsWith("image/")) {
1816
1856
  media = await this.processImageAttachment(attachment);
1817
- } else if (attachment.contentType?.startsWith("video/") || this.runtime.getService(ServiceType3.VIDEO).isVideoUrl(attachment.url)) {
1857
+ } else if (attachment.contentType?.startsWith("video/") || this.runtime.getService(ServiceType3.VIDEO)?.isVideoUrl(attachment.url)) {
1818
1858
  media = await this.processVideoAttachment(attachment);
1819
1859
  } else {
1820
1860
  media = await this.processGenericAttachment(attachment);
@@ -1852,7 +1892,9 @@ var AttachmentManager = class {
1852
1892
  text: transcription || "Audio/video content not available"
1853
1893
  };
1854
1894
  } catch (error) {
1855
- console.error(`Error processing audio/video attachment: ${error.message}`);
1895
+ if (error instanceof Error) {
1896
+ console.error(`Error processing audio/video attachment: ${error.message}`);
1897
+ }
1856
1898
  return {
1857
1899
  id: attachment.id,
1858
1900
  url: attachment.url,
@@ -1907,7 +1949,11 @@ var AttachmentManager = class {
1907
1949
  try {
1908
1950
  const response = await fetch(attachment.url);
1909
1951
  const pdfBuffer = await response.arrayBuffer();
1910
- const text = await this.runtime.getService(ServiceType3.PDF).convertPdfToText(Buffer.from(pdfBuffer));
1952
+ const pdfService = this.runtime.getService(ServiceType3.PDF);
1953
+ if (!pdfService) {
1954
+ throw new Error("PDF service not found");
1955
+ }
1956
+ const text = await pdfService.convertPdfToText(Buffer.from(pdfBuffer));
1911
1957
  const { title, description } = await generateSummary(this.runtime, text);
1912
1958
  return {
1913
1959
  id: attachment.id,
@@ -1918,7 +1964,9 @@ var AttachmentManager = class {
1918
1964
  text
1919
1965
  };
1920
1966
  } catch (error) {
1921
- console.error(`Error processing PDF attachment: ${error.message}`);
1967
+ if (error instanceof Error) {
1968
+ console.error(`Error processing PDF attachment: ${error.message}`);
1969
+ }
1922
1970
  return {
1923
1971
  id: attachment.id,
1924
1972
  url: attachment.url,
@@ -1948,7 +1996,11 @@ var AttachmentManager = class {
1948
1996
  text
1949
1997
  };
1950
1998
  } catch (error) {
1951
- console.error(`Error processing plaintext attachment: ${error.message}`);
1999
+ if (error instanceof Error) {
2000
+ console.error(`Error processing plaintext attachment: ${error.message}`);
2001
+ } else {
2002
+ console.error(`An unknown error occurred during plaintext attachment processing`);
2003
+ }
1952
2004
  return {
1953
2005
  id: attachment.id,
1954
2006
  url: attachment.url,
@@ -1982,7 +2034,9 @@ var AttachmentManager = class {
1982
2034
  text: description || "Image content not available"
1983
2035
  };
1984
2036
  } catch (error) {
1985
- console.error(`Error processing image attachment: ${error.message}`);
2037
+ if (error instanceof Error) {
2038
+ console.error(`Error processing image attachment: ${error.message}`);
2039
+ }
1986
2040
  return this.createFallbackImageMedia(attachment);
1987
2041
  }
1988
2042
  }
@@ -2011,9 +2065,16 @@ var AttachmentManager = class {
2011
2065
  async processVideoAttachment(attachment) {
2012
2066
  const videoService = this.runtime.getService(ServiceType3.VIDEO);
2013
2067
  if (!videoService) {
2014
- throw new Error("Video service not found");
2068
+ return {
2069
+ id: attachment.id,
2070
+ url: attachment.url,
2071
+ title: "Video Attachment (Service Unavailable)",
2072
+ source: "Video",
2073
+ description: "Could not process video attachment because the required service is not available.",
2074
+ text: "Video content not available"
2075
+ };
2015
2076
  }
2016
- if (videoService.isVideoUrl(attachment.url)) {
2077
+ if (typeof videoService.isVideoUrl === "function" && videoService.isVideoUrl(attachment.url)) {
2017
2078
  const videoInfo = await videoService.processVideo(attachment.url, this.runtime);
2018
2079
  return {
2019
2080
  id: attachment.id,
@@ -2062,43 +2123,103 @@ import {
2062
2123
  PermissionsBitField,
2063
2124
  ThreadChannel
2064
2125
  } from "discord.js";
2065
- function getWavHeader(audioLength, sampleRate, channelCount = 1, bitsPerSample = 16) {
2066
- const wavHeader = Buffer.alloc(44);
2067
- wavHeader.write("RIFF", 0);
2068
- wavHeader.writeUInt32LE(36 + audioLength, 4);
2069
- wavHeader.write("WAVE", 8);
2070
- wavHeader.write("fmt ", 12);
2071
- wavHeader.writeUInt32LE(16, 16);
2072
- wavHeader.writeUInt16LE(1, 20);
2073
- wavHeader.writeUInt16LE(channelCount, 22);
2074
- wavHeader.writeUInt32LE(sampleRate, 24);
2075
- wavHeader.writeUInt32LE(sampleRate * bitsPerSample * channelCount / 8, 28);
2076
- wavHeader.writeUInt16LE(bitsPerSample * channelCount / 8, 32);
2077
- wavHeader.writeUInt16LE(bitsPerSample, 34);
2078
- wavHeader.write("data", 36);
2079
- wavHeader.writeUInt32LE(audioLength, 40);
2080
- return wavHeader;
2081
- }
2082
2126
  var MAX_MESSAGE_LENGTH = 1900;
2083
- async function sendMessageInChunks(channel, content, _inReplyTo, files) {
2127
+ async function sendMessageInChunks(channel, content, _inReplyTo, files, components) {
2084
2128
  const sentMessages = [];
2085
2129
  const messages = splitMessage(content);
2086
2130
  try {
2087
2131
  for (let i = 0; i < messages.length; i++) {
2088
2132
  const message = messages[i];
2089
- if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0) {
2133
+ if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0 || components) {
2090
2134
  const options = {
2091
2135
  content: message.trim()
2092
2136
  };
2093
2137
  if (i === messages.length - 1 && files && files.length > 0) {
2094
2138
  options.files = files;
2095
2139
  }
2140
+ if (i === messages.length - 1 && components && components.length > 0) {
2141
+ try {
2142
+ const safeStringify = (obj) => {
2143
+ return JSON.stringify(
2144
+ obj,
2145
+ (_, value) => typeof value === "bigint" ? value.toString() : value
2146
+ );
2147
+ };
2148
+ logger3.info(`Components received: ${safeStringify(components)}`);
2149
+ if (!Array.isArray(components)) {
2150
+ logger3.warn("Components is not an array, skipping component processing");
2151
+ } else if (components.length > 0 && components[0] && typeof components[0].toJSON === "function") {
2152
+ options.components = components;
2153
+ } else {
2154
+ const {
2155
+ ActionRowBuilder,
2156
+ ButtonBuilder,
2157
+ StringSelectMenuBuilder
2158
+ } = __require("discord.js");
2159
+ const discordComponents = components.map((row) => {
2160
+ if (!row || typeof row !== "object" || row.type !== 1) {
2161
+ logger3.warn("Invalid component row structure, skipping");
2162
+ return null;
2163
+ }
2164
+ if (row.type === 1) {
2165
+ const actionRow = new ActionRowBuilder();
2166
+ if (!Array.isArray(row.components)) {
2167
+ logger3.warn("Row components is not an array, skipping");
2168
+ return null;
2169
+ }
2170
+ const validComponents = row.components.map((comp) => {
2171
+ if (!comp || typeof comp !== "object") {
2172
+ logger3.warn("Invalid component, skipping");
2173
+ return null;
2174
+ }
2175
+ try {
2176
+ if (comp.type === 2) {
2177
+ return new ButtonBuilder().setCustomId(comp.custom_id).setLabel(comp.label || "").setStyle(comp.style || 1);
2178
+ }
2179
+ if (comp.type === 3) {
2180
+ const selectMenu = new StringSelectMenuBuilder().setCustomId(comp.custom_id).setPlaceholder(comp.placeholder || "Select an option");
2181
+ if (typeof comp.min_values === "number")
2182
+ selectMenu.setMinValues(comp.min_values);
2183
+ if (typeof comp.max_values === "number")
2184
+ selectMenu.setMaxValues(comp.max_values);
2185
+ if (Array.isArray(comp.options)) {
2186
+ selectMenu.addOptions(
2187
+ comp.options.map((option) => ({
2188
+ label: option.label,
2189
+ value: option.value,
2190
+ description: option.description
2191
+ }))
2192
+ );
2193
+ }
2194
+ return selectMenu;
2195
+ }
2196
+ } catch (err) {
2197
+ logger3.error(`Error creating component: ${err}`);
2198
+ return null;
2199
+ }
2200
+ return null;
2201
+ }).filter(Boolean);
2202
+ if (validComponents.length > 0) {
2203
+ actionRow.addComponents(validComponents);
2204
+ return actionRow;
2205
+ }
2206
+ }
2207
+ return null;
2208
+ }).filter(Boolean);
2209
+ if (discordComponents.length > 0) {
2210
+ options.components = discordComponents;
2211
+ }
2212
+ }
2213
+ } catch (error) {
2214
+ logger3.error(`Error processing components: ${error}`);
2215
+ }
2216
+ }
2096
2217
  const m = await channel.send(options);
2097
2218
  sentMessages.push(m);
2098
2219
  }
2099
2220
  }
2100
2221
  } catch (error) {
2101
- logger3.error("Error sending message:", error);
2222
+ logger3.error(`Error sending message: ${error}`);
2102
2223
  }
2103
2224
  return sentMessages;
2104
2225
  }
@@ -2207,6 +2328,9 @@ var MessageManager = class {
2207
2328
  if (this.runtime.character.settings?.discord?.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType2.DM) {
2208
2329
  return;
2209
2330
  }
2331
+ if (this.runtime.character.settings?.discord?.shouldRespondOnlyToMentions && (!this.client.user?.id || !message.mentions.users?.has(this.client.user.id))) {
2332
+ return;
2333
+ }
2210
2334
  const entityId = createUniqueUuid4(this.runtime, message.author.id);
2211
2335
  const userName = message.author.bot ? `${message.author.username}#${message.author.discriminator}` : message.author.username;
2212
2336
  const name = message.author.displayName;
@@ -2217,6 +2341,9 @@ var MessageManager = class {
2217
2341
  if (message.guild) {
2218
2342
  const guild = await message.guild.fetch();
2219
2343
  type = await this.getChannelType(message.channel);
2344
+ if (type === null) {
2345
+ logger4.warn("null channel type, discord message", message);
2346
+ }
2220
2347
  serverId = guild.id;
2221
2348
  } else {
2222
2349
  type = ChannelType7.DM;
@@ -2230,7 +2357,9 @@ var MessageManager = class {
2230
2357
  source: "discord",
2231
2358
  channelId: message.channel.id,
2232
2359
  serverId,
2233
- type
2360
+ type,
2361
+ worldId: createUniqueUuid4(this.runtime, serverId ?? roomId),
2362
+ worldName: message.guild?.name
2234
2363
  });
2235
2364
  try {
2236
2365
  const canSendResult = canSendMessage(message.channel);
@@ -2250,6 +2379,22 @@ var MessageManager = class {
2250
2379
  }
2251
2380
  const entityId2 = createUniqueUuid4(this.runtime, message.author.id);
2252
2381
  const messageId = createUniqueUuid4(this.runtime, message.id);
2382
+ const channel = message.channel;
2383
+ const startTyping = () => {
2384
+ try {
2385
+ if (channel.sendTyping) {
2386
+ channel.sendTyping();
2387
+ }
2388
+ } catch (err) {
2389
+ logger4.warn("Error sending typing indicator:", err);
2390
+ }
2391
+ };
2392
+ startTyping();
2393
+ const typingInterval = setInterval(startTyping, 8e3);
2394
+ const typingData = {
2395
+ interval: typingInterval,
2396
+ cleared: false
2397
+ };
2253
2398
  const newMessage = {
2254
2399
  id: messageId,
2255
2400
  entityId: entityId2,
@@ -2264,6 +2409,10 @@ var MessageManager = class {
2264
2409
  url: message.url,
2265
2410
  inReplyTo: message.reference?.messageId ? createUniqueUuid4(this.runtime, message.reference?.messageId) : void 0
2266
2411
  },
2412
+ metadata: {
2413
+ entityName: name,
2414
+ type: "message"
2415
+ },
2267
2416
  createdAt: message.createdTimestamp
2268
2417
  };
2269
2418
  const callback = async (content, files) => {
@@ -2271,37 +2420,54 @@ var MessageManager = class {
2271
2420
  if (message.id && !content.inReplyTo) {
2272
2421
  content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
2273
2422
  }
2274
- const messages = await sendMessageInChunks(
2275
- message.channel,
2276
- content.text,
2277
- message.id,
2278
- files
2279
- );
2280
- const memories = [];
2281
- for (const m of messages) {
2282
- const actions = content.actions;
2283
- const memory = {
2284
- id: createUniqueUuid4(this.runtime, m.id),
2285
- entityId: this.runtime.agentId,
2286
- agentId: this.runtime.agentId,
2287
- content: {
2288
- ...content,
2289
- actions,
2290
- inReplyTo: messageId,
2291
- url: m.url,
2292
- channelType: type
2293
- },
2294
- roomId,
2295
- createdAt: m.createdTimestamp
2296
- };
2297
- memories.push(memory);
2298
- }
2299
- for (const m of memories) {
2300
- await this.runtime.createMemory(m, "messages");
2423
+ try {
2424
+ const messages = await sendMessageInChunks(
2425
+ channel,
2426
+ content.text ?? "",
2427
+ message.id,
2428
+ files
2429
+ );
2430
+ const memories = [];
2431
+ for (const m of messages) {
2432
+ const actions = content.actions;
2433
+ const memory = {
2434
+ id: createUniqueUuid4(this.runtime, m.id),
2435
+ entityId: this.runtime.agentId,
2436
+ agentId: this.runtime.agentId,
2437
+ content: {
2438
+ ...content,
2439
+ actions,
2440
+ inReplyTo: messageId,
2441
+ url: m.url,
2442
+ channelType: type
2443
+ },
2444
+ roomId,
2445
+ createdAt: m.createdTimestamp
2446
+ };
2447
+ memories.push(memory);
2448
+ }
2449
+ for (const m of memories) {
2450
+ await this.runtime.createMemory(m, "messages");
2451
+ }
2452
+ if (typingData.interval && !typingData.cleared) {
2453
+ clearInterval(typingData.interval);
2454
+ typingData.cleared = true;
2455
+ }
2456
+ return memories;
2457
+ } catch (error) {
2458
+ console.error("Error sending message:", error);
2459
+ if (typingData.interval && !typingData.cleared) {
2460
+ clearInterval(typingData.interval);
2461
+ typingData.cleared = true;
2462
+ }
2463
+ return [];
2301
2464
  }
2302
- return memories;
2303
2465
  } catch (error) {
2304
- console.error("Error sending message:", error);
2466
+ console.error("Error handling message:", error);
2467
+ if (typingData.interval && !typingData.cleared) {
2468
+ clearInterval(typingData.interval);
2469
+ typingData.cleared = true;
2470
+ }
2305
2471
  return [];
2306
2472
  }
2307
2473
  };
@@ -2310,6 +2476,12 @@ var MessageManager = class {
2310
2476
  message: newMessage,
2311
2477
  callback
2312
2478
  });
2479
+ setTimeout(() => {
2480
+ if (typingData.interval && !typingData.cleared) {
2481
+ clearInterval(typingData.interval);
2482
+ typingData.cleared = true;
2483
+ }
2484
+ }, 500);
2313
2485
  } catch (error) {
2314
2486
  console.error("Error handling message:", error);
2315
2487
  }
@@ -2356,11 +2528,8 @@ var MessageManager = class {
2356
2528
  const urlRegex = /(https?:\/\/[^\s]+)/g;
2357
2529
  const urls = processedContent.match(urlRegex) || [];
2358
2530
  for (const url of urls) {
2359
- if (this.runtime.getService(ServiceType4.VIDEO)?.isVideoUrl(url)) {
2360
- const videoService = this.runtime.getService(ServiceType4.VIDEO);
2361
- if (!videoService) {
2362
- throw new Error("Video service not found");
2363
- }
2531
+ const videoService = this.runtime.getService(ServiceType4.VIDEO);
2532
+ if (videoService?.isVideoUrl(url)) {
2364
2533
  const videoInfo = await videoService.processVideo(url, this.runtime);
2365
2534
  attachments.push({
2366
2535
  id: `youtube-${Date.now()}`,
@@ -2373,7 +2542,8 @@ var MessageManager = class {
2373
2542
  } else {
2374
2543
  const browserService = this.runtime.getService(ServiceType4.BROWSER);
2375
2544
  if (!browserService) {
2376
- throw new Error("Browser service not found");
2545
+ logger4.warn("Browser service not found");
2546
+ continue;
2377
2547
  }
2378
2548
  const { title, description: summary } = await browserService.getPageContent(
2379
2549
  url,
@@ -2432,6 +2602,7 @@ import {
2432
2602
  ChannelType as ChannelType8,
2433
2603
  ModelType as ModelType8,
2434
2604
  createUniqueUuid as createUniqueUuid5,
2605
+ getWavHeader,
2435
2606
  logger as logger5
2436
2607
  } from "@elizaos/core";
2437
2608
  import {
@@ -2561,9 +2732,17 @@ var VoiceManager = class extends EventEmitter {
2561
2732
  super();
2562
2733
  this.client = service.client;
2563
2734
  this.runtime = runtime;
2564
- this.client.on("voiceManagerReady", () => {
2565
- this.setReady(true);
2566
- });
2735
+ this.ready = false;
2736
+ if (this.client) {
2737
+ this.client.on("voiceManagerReady", () => {
2738
+ this.setReady(true);
2739
+ });
2740
+ } else {
2741
+ logger5.error(
2742
+ "Discord client is not available in VoiceManager constructor for voiceManagerReady event"
2743
+ );
2744
+ this.ready = false;
2745
+ }
2567
2746
  }
2568
2747
  /**
2569
2748
  * Asynchronously retrieves the type of the channel.
@@ -2575,6 +2754,11 @@ var VoiceManager = class extends EventEmitter {
2575
2754
  case DiscordChannelType3.GuildVoice:
2576
2755
  case DiscordChannelType3.GuildStageVoice:
2577
2756
  return ChannelType8.VOICE_GROUP;
2757
+ default:
2758
+ logger5.error(
2759
+ `getChannelType received unexpected channel type: ${channel.type} for channel ${channel.id}`
2760
+ );
2761
+ throw new Error(`Unexpected channel type encountered: ${channel.type}`);
2578
2762
  }
2579
2763
  }
2580
2764
  /**
@@ -2605,7 +2789,7 @@ var VoiceManager = class extends EventEmitter {
2605
2789
  const newChannelId = newState.channelId;
2606
2790
  const member = newState.member;
2607
2791
  if (!member) return;
2608
- if (member.id === this.client.user?.id) {
2792
+ if (member.id === this.client?.user?.id) {
2609
2793
  return;
2610
2794
  }
2611
2795
  if (oldChannelId === newChannelId) {
@@ -2639,7 +2823,7 @@ var VoiceManager = class extends EventEmitter {
2639
2823
  adapterCreator: channel.guild.voiceAdapterCreator,
2640
2824
  selfDeaf: false,
2641
2825
  selfMute: false,
2642
- group: this.client.user.id
2826
+ group: this.client?.user?.id ?? "default-group"
2643
2827
  });
2644
2828
  try {
2645
2829
  await Promise.race([
@@ -2715,7 +2899,12 @@ var VoiceManager = class extends EventEmitter {
2715
2899
  * @returns {VoiceConnection | undefined} The voice connection for the specified guild ID, or undefined if not found.
2716
2900
  */
2717
2901
  getVoiceConnection(guildId) {
2718
- const connections = getVoiceConnections(this.client.user.id);
2902
+ const userId = this.client?.user?.id;
2903
+ if (!userId) {
2904
+ logger5.error("Client user ID is not available.");
2905
+ return void 0;
2906
+ }
2907
+ const connections = getVoiceConnections(userId);
2719
2908
  if (!connections) {
2720
2909
  return;
2721
2910
  }
@@ -2740,6 +2929,7 @@ var VoiceManager = class extends EventEmitter {
2740
2929
  emitClose: true
2741
2930
  });
2742
2931
  if (!receiveStream || receiveStream.readableLength === 0) {
2932
+ logger5.warn(`[monitorMember] No receiveStream or empty stream for user ${entityId}`);
2743
2933
  return;
2744
2934
  }
2745
2935
  const opusDecoder = new prism.opus.Decoder({
@@ -2768,7 +2958,11 @@ var VoiceManager = class extends EventEmitter {
2768
2958
  });
2769
2959
  pipeline(receiveStream, opusDecoder, (err) => {
2770
2960
  if (err) {
2771
- logger5.debug(`Opus decoding pipeline error: ${err}`);
2961
+ logger5.debug(`[monitorMember] Opus decoding pipeline error for user ${entityId}: ${err}`);
2962
+ } else {
2963
+ logger5.debug(
2964
+ `[monitorMember] Opus decoding pipeline finished successfully for user ${entityId}`
2965
+ );
2772
2966
  }
2773
2967
  });
2774
2968
  this.streams.set(entityId, opusDecoder);
@@ -2793,7 +2987,7 @@ var VoiceManager = class extends EventEmitter {
2793
2987
  opusDecoder.on("error", errorHandler);
2794
2988
  opusDecoder.on("close", closeHandler);
2795
2989
  receiveStream?.on("close", streamCloseHandler);
2796
- this.client.emit("userStream", entityId, name, userName, channel, opusDecoder);
2990
+ this.client?.emit("userStream", entityId, name, userName, channel, opusDecoder);
2797
2991
  }
2798
2992
  /**
2799
2993
  * Leaves the specified voice channel and stops monitoring all members in that channel.
@@ -2808,7 +3002,7 @@ var VoiceManager = class extends EventEmitter {
2808
3002
  this.connections.delete(channel.id);
2809
3003
  }
2810
3004
  for (const [memberId, monitorInfo] of this.activeMonitors) {
2811
- if (monitorInfo.channel.id === channel.id && memberId !== this.client.user?.id) {
3005
+ if (monitorInfo.channel.id === channel.id && memberId !== this.client?.user?.id) {
2812
3006
  this.stopMonitoringMember(memberId);
2813
3007
  }
2814
3008
  }
@@ -2843,8 +3037,10 @@ var VoiceManager = class extends EventEmitter {
2843
3037
  }
2844
3038
  if (this.activeAudioPlayer || this.processingVoice) {
2845
3039
  const state = this.userStates.get(entityId);
2846
- state.buffers.length = 0;
2847
- state.totalLength = 0;
3040
+ if (state) {
3041
+ state.buffers.length = 0;
3042
+ state.totalLength = 0;
3043
+ }
2848
3044
  return;
2849
3045
  }
2850
3046
  if (this.transcriptionTimeout) {
@@ -2872,8 +3068,7 @@ var VoiceManager = class extends EventEmitter {
2872
3068
  * @param {BaseGuildVoiceChannel} channel - The voice channel the user is in.
2873
3069
  * @param {Readable} audioStream - The audio stream to monitor.
2874
3070
  */
2875
- async handleUserStream(userId, name, userName, channel, audioStream) {
2876
- const entityId = createUniqueUuid5(this.runtime, userId);
3071
+ async handleUserStream(entityId, name, userName, channel, audioStream) {
2877
3072
  logger5.debug(`Starting audio monitor for user: ${entityId}`);
2878
3073
  if (!this.userStates.has(entityId)) {
2879
3074
  this.userStates.set(entityId, {
@@ -2925,17 +3120,18 @@ var VoiceManager = class extends EventEmitter {
2925
3120
  const state = this.userStates.get(entityId);
2926
3121
  if (!state || state.buffers.length === 0) return;
2927
3122
  try {
2928
- let isValidTranscription = function(text) {
3123
+ let isValidTranscription2 = function(text) {
2929
3124
  if (!text || text.includes("[BLANK_AUDIO]")) return false;
2930
3125
  return true;
2931
3126
  };
3127
+ var isValidTranscription = isValidTranscription2;
2932
3128
  const inputBuffer = Buffer.concat(state.buffers, state.totalLength);
2933
3129
  state.buffers.length = 0;
2934
3130
  state.totalLength = 0;
2935
3131
  const wavBuffer = await this.convertOpusToWav(inputBuffer);
2936
3132
  logger5.debug("Starting transcription...");
2937
3133
  const transcriptionText = await this.runtime.useModel(ModelType8.TRANSCRIPTION, wavBuffer);
2938
- if (transcriptionText && isValidTranscription(transcriptionText)) {
3134
+ if (transcriptionText && isValidTranscription2(transcriptionText)) {
2939
3135
  state.transcriptionText += transcriptionText;
2940
3136
  }
2941
3137
  if (state.transcriptionText.length) {
@@ -2965,21 +3161,24 @@ var VoiceManager = class extends EventEmitter {
2965
3161
  return { text: "", actions: ["IGNORE"] };
2966
3162
  }
2967
3163
  const roomId = createUniqueUuid5(this.runtime, channelId);
3164
+ const uniqueEntityId = createUniqueUuid5(this.runtime, entityId);
2968
3165
  const type = await this.getChannelType(channel);
2969
3166
  await this.runtime.ensureConnection({
2970
- entityId,
3167
+ entityId: uniqueEntityId,
2971
3168
  roomId,
2972
3169
  userName,
2973
3170
  name,
2974
3171
  source: "discord",
2975
3172
  channelId,
2976
3173
  serverId: channel.guild.id,
2977
- type
3174
+ type,
3175
+ worldId: createUniqueUuid5(this.runtime, channel.guild.id),
3176
+ worldName: channel.guild.name
2978
3177
  });
2979
3178
  const memory = {
2980
3179
  id: createUniqueUuid5(this.runtime, `${channelId}-voice-message-${Date.now()}`),
2981
3180
  agentId: this.runtime.agentId,
2982
- entityId,
3181
+ entityId: uniqueEntityId,
2983
3182
  roomId,
2984
3183
  content: {
2985
3184
  text: message,
@@ -3009,7 +3208,7 @@ var VoiceManager = class extends EventEmitter {
3009
3208
  createdAt: Date.now()
3010
3209
  };
3011
3210
  if (responseMemory.content.text?.trim()) {
3012
- await this.runtime.createMemory(responseMemory);
3211
+ await this.runtime.createMemory(responseMemory, "messages");
3013
3212
  const responseStream = await this.runtime.useModel(
3014
3213
  ModelType8.TEXT_TO_SPEECH,
3015
3214
  content.text
@@ -3198,6 +3397,13 @@ var DiscordService = class _DiscordService extends Service {
3198
3397
  character;
3199
3398
  messageManager;
3200
3399
  voiceManager;
3400
+ userSelections = /* @__PURE__ */ new Map();
3401
+ timeouts = [];
3402
+ /**
3403
+ * List of allowed channel IDs (parsed from CHANNEL_IDS env var).
3404
+ * If undefined, all channels are allowed.
3405
+ */
3406
+ allowedChannelIds;
3201
3407
  /**
3202
3408
  * Constructor for Discord client.
3203
3409
  * Initializes the Discord client with specified intents and partials,
@@ -3207,6 +3413,11 @@ var DiscordService = class _DiscordService extends Service {
3207
3413
  */
3208
3414
  constructor(runtime) {
3209
3415
  super(runtime);
3416
+ this.character = runtime.character;
3417
+ const channelIdsRaw = runtime.getSetting("CHANNEL_IDS");
3418
+ if (channelIdsRaw && channelIdsRaw.trim()) {
3419
+ this.allowedChannelIds = channelIdsRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3420
+ }
3210
3421
  const token = runtime.getSetting("DISCORD_API_TOKEN");
3211
3422
  if (!token || token.trim() === "") {
3212
3423
  logger6.warn("Discord API Token not provided - Discord functionality will be unavailable");
@@ -3232,19 +3443,135 @@ var DiscordService = class _DiscordService extends Service {
3232
3443
  this.runtime = runtime;
3233
3444
  this.voiceManager = new VoiceManager(this, runtime);
3234
3445
  this.messageManager = new MessageManager(this);
3235
- this.client.once(Events.ClientReady, this.onClientReady.bind(this));
3446
+ this.client.once(Events.ClientReady, this.onReady.bind(this));
3236
3447
  this.client.login(token).catch((error) => {
3237
- logger6.error(`Failed to login to Discord: ${error.message}`);
3448
+ logger6.error(
3449
+ `Failed to login to Discord: ${error instanceof Error ? error.message : String(error)}`
3450
+ );
3238
3451
  this.client = null;
3239
3452
  });
3240
3453
  this.setupEventListeners();
3454
+ this.registerSendHandler();
3241
3455
  } catch (error) {
3242
- logger6.error(`Error initializing Discord client: ${error.message}`);
3456
+ logger6.error(
3457
+ `Error initializing Discord client: ${error instanceof Error ? error.message : String(error)}`
3458
+ );
3243
3459
  this.client = null;
3244
3460
  }
3245
3461
  }
3462
+ static async start(runtime) {
3463
+ const service = new _DiscordService(runtime);
3464
+ return service;
3465
+ }
3466
+ /**
3467
+ * Registers the send handler with the runtime.
3468
+ * @private
3469
+ */
3470
+ registerSendHandler() {
3471
+ if (this.runtime) {
3472
+ this.runtime.registerSendHandler("discord", this.handleSendMessage.bind(this));
3473
+ }
3474
+ }
3475
+ /**
3476
+ * The SendHandlerFunction implementation for Discord.
3477
+ * @param {IAgentRuntime} runtime - The runtime instance.
3478
+ * @param {TargetInfo} target - The target information for the message.
3479
+ * @param {Content} content - The content of the message to send.
3480
+ * @returns {Promise<void>} A promise that resolves when the message is sent or rejects on error.
3481
+ * @throws {Error} If the client is not ready, target is invalid, or sending fails.
3482
+ */
3483
+ async handleSendMessage(runtime, target, content) {
3484
+ if (!this.client?.isReady()) {
3485
+ logger6.error("[Discord SendHandler] Client not ready.");
3486
+ throw new Error("Discord client is not ready.");
3487
+ }
3488
+ if (target.channelId && this.allowedChannelIds && !this.allowedChannelIds.includes(target.channelId)) {
3489
+ logger6.warn(
3490
+ `[Discord SendHandler] Channel ${target.channelId} is not in allowed channels, skipping send.`
3491
+ );
3492
+ return;
3493
+ }
3494
+ let targetChannel = null;
3495
+ try {
3496
+ if (target.channelId) {
3497
+ targetChannel = await this.client.channels.fetch(target.channelId);
3498
+ } else if (target.entityId) {
3499
+ const discordUserId = target.entityId;
3500
+ const user = await this.client.users.fetch(discordUserId);
3501
+ if (user) {
3502
+ targetChannel = await user.dmChannel ?? await user.createDM();
3503
+ }
3504
+ } else {
3505
+ throw new Error("Discord SendHandler requires channelId or entityId.");
3506
+ }
3507
+ if (!targetChannel) {
3508
+ throw new Error(
3509
+ `Could not find target Discord channel/DM for target: ${JSON.stringify(target)}`
3510
+ );
3511
+ }
3512
+ if (targetChannel.isTextBased() && !targetChannel.isVoiceBased()) {
3513
+ if ("send" in targetChannel && typeof targetChannel.send === "function") {
3514
+ if (content.text) {
3515
+ const chunks = this.splitMessage(content.text, 2e3);
3516
+ for (const chunk of chunks) {
3517
+ await targetChannel.send(chunk);
3518
+ }
3519
+ } else {
3520
+ logger6.warn("[Discord SendHandler] No text content provided to send.");
3521
+ }
3522
+ } else {
3523
+ throw new Error(`Target channel ${targetChannel.id} does not have a send method.`);
3524
+ }
3525
+ } else {
3526
+ throw new Error(
3527
+ `Target channel ${targetChannel.id} is not a valid text-based channel for sending messages.`
3528
+ );
3529
+ }
3530
+ } catch (error) {
3531
+ logger6.error(
3532
+ `[Discord SendHandler] Error sending message: ${error instanceof Error ? error.message : String(error)}`,
3533
+ {
3534
+ target,
3535
+ content
3536
+ }
3537
+ );
3538
+ throw error;
3539
+ }
3540
+ }
3541
+ /**
3542
+ * Helper function to split a string into chunks of a maximum length.
3543
+ *
3544
+ * @param {string} text - The text to split.
3545
+ * @param {number} maxLength - The maximum length of each chunk.
3546
+ * @returns {string[]} An array of text chunks.
3547
+ * @private
3548
+ */
3549
+ // Helper to split messages
3550
+ splitMessage(text, maxLength) {
3551
+ const chunks = [];
3552
+ let currentChunk = "";
3553
+ const lines = text.split("\n");
3554
+ for (const line of lines) {
3555
+ if (currentChunk.length + line.length + 1 <= maxLength) {
3556
+ currentChunk += (currentChunk ? "\n" : "") + line;
3557
+ } else {
3558
+ if (currentChunk) chunks.push(currentChunk);
3559
+ if (line.length > maxLength) {
3560
+ for (let i = 0; i < line.length; i += maxLength) {
3561
+ chunks.push(line.substring(i, i + maxLength));
3562
+ }
3563
+ currentChunk = "";
3564
+ } else {
3565
+ currentChunk = line;
3566
+ }
3567
+ }
3568
+ }
3569
+ if (currentChunk) chunks.push(currentChunk);
3570
+ return chunks;
3571
+ }
3246
3572
  /**
3247
- * Set up event listeners for the client
3573
+ * Set up event listeners for the client.
3574
+ * @private
3248
3575
  */
3249
3576
  setupEventListeners() {
3250
3577
  if (!this.client) {
@@ -3254,8 +3581,11 @@ var DiscordService = class _DiscordService extends Service {
3254
3581
  if (message.author.id === this.client?.user?.id || message.author.bot) {
3255
3582
  return;
3256
3583
  }
3584
+ if (this.allowedChannelIds && !this.allowedChannelIds.includes(message.channel.id)) {
3585
+ return;
3586
+ }
3257
3587
  try {
3258
- this.messageManager.handleMessage(message);
3588
+ this.messageManager?.handleMessage(message);
3259
3589
  } catch (error) {
3260
3590
  logger6.error(`Error handling message: ${error}`);
3261
3591
  }
@@ -3264,6 +3594,9 @@ var DiscordService = class _DiscordService extends Service {
3264
3594
  if (user.id === this.client?.user?.id) {
3265
3595
  return;
3266
3596
  }
3597
+ if (this.allowedChannelIds && reaction.message.channel && !this.allowedChannelIds.includes(reaction.message.channel.id)) {
3598
+ return;
3599
+ }
3267
3600
  try {
3268
3601
  await this.handleReactionAdd(reaction, user);
3269
3602
  } catch (error) {
@@ -3274,6 +3607,9 @@ var DiscordService = class _DiscordService extends Service {
3274
3607
  if (user.id === this.client?.user?.id) {
3275
3608
  return;
3276
3609
  }
3610
+ if (this.allowedChannelIds && reaction.message.channel && !this.allowedChannelIds.includes(reaction.message.channel.id)) {
3611
+ return;
3612
+ }
3277
3613
  try {
3278
3614
  await this.handleReactionRemove(reaction, user);
3279
3615
  } catch (error) {
@@ -3295,27 +3631,38 @@ var DiscordService = class _DiscordService extends Service {
3295
3631
  }
3296
3632
  });
3297
3633
  this.client.on("interactionCreate", async (interaction) => {
3634
+ if (this.allowedChannelIds && interaction.channelId && !this.allowedChannelIds.includes(interaction.channelId)) {
3635
+ return;
3636
+ }
3298
3637
  try {
3299
3638
  await this.handleInteractionCreate(interaction);
3300
3639
  } catch (error) {
3301
3640
  logger6.error(`Error handling interaction: ${error}`);
3302
3641
  }
3303
3642
  });
3643
+ this.client.on("userStream", (entityId, name, userName, channel, opusDecoder) => {
3644
+ if (entityId !== this.client?.user?.id) {
3645
+ this.voiceManager?.handleUserStream(entityId, name, userName, channel, opusDecoder);
3646
+ }
3647
+ });
3304
3648
  }
3305
3649
  /**
3306
3650
  * Handles the event when a new member joins a guild.
3307
3651
  *
3308
3652
  * @param {GuildMember} member - The GuildMember object representing the new member that joined the guild.
3309
3653
  * @returns {Promise<void>} - A Promise that resolves once the event handling is complete.
3654
+ * @private
3310
3655
  */
3311
3656
  async handleGuildMemberAdd(member) {
3312
3657
  logger6.log(`New member joined: ${member.user.username}`);
3313
3658
  const guild = member.guild;
3314
3659
  const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3660
+ const worldId = createUniqueUuid6(this.runtime, guild.id);
3661
+ const entityId = createUniqueUuid6(this.runtime, member.id);
3315
3662
  this.runtime.emitEvent([EventType2.ENTITY_JOINED], {
3316
3663
  runtime: this.runtime,
3317
- entityId: createUniqueUuid6(this.runtime, member.id),
3318
- worldId: createUniqueUuid6(this.runtime, guild.id),
3664
+ entityId,
3665
+ worldId,
3319
3666
  source: "discord",
3320
3667
  metadata: {
3321
3668
  originalId: member.id,
@@ -3327,170 +3674,449 @@ var DiscordService = class _DiscordService extends Service {
3327
3674
  });
3328
3675
  this.runtime.emitEvent(["DISCORD_USER_JOINED" /* ENTITY_JOINED */], {
3329
3676
  runtime: this.runtime,
3330
- entityId: createUniqueUuid6(this.runtime, member.id),
3677
+ entityId,
3678
+ worldId,
3331
3679
  member,
3332
3680
  guild
3333
3681
  });
3334
3682
  }
3335
3683
  /**
3336
- *
3337
- * Start the Discord service
3338
- * @param {IAgentRuntime} runtime - The runtime for the agent
3339
- * @returns {Promise<DiscordService>} A promise that resolves to a DiscordService instance
3340
- *
3684
+ * Handles the event when the bot joins a guild. It logs the guild name, fetches additional information about the guild, scans the guild for voice data, creates standardized world data structure, generates unique IDs, and emits events to the runtime.
3685
+ * @param {Guild} guild - The guild that the bot has joined.
3686
+ * @returns {Promise<void>} A promise that resolves when the guild creation is handled.
3687
+ * @private
3341
3688
  */
3342
- static async start(runtime) {
3343
- const token = runtime.getSetting("DISCORD_API_TOKEN");
3344
- if (!token || token.trim() === "") {
3345
- throw new Error("Discord API Token not provided");
3689
+ async handleGuildCreate(guild) {
3690
+ logger6.log(`Joined guild ${guild.name}`);
3691
+ const fullGuild = await guild.fetch();
3692
+ this.voiceManager?.scanGuild(guild);
3693
+ const ownerId = createUniqueUuid6(this.runtime, fullGuild.ownerId);
3694
+ const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
3695
+ const standardizedData = {
3696
+ runtime: this.runtime,
3697
+ rooms: await this.buildStandardizedRooms(fullGuild, worldId),
3698
+ users: await this.buildStandardizedUsers(fullGuild),
3699
+ world: {
3700
+ id: worldId,
3701
+ name: fullGuild.name,
3702
+ agentId: this.runtime.agentId,
3703
+ serverId: fullGuild.id,
3704
+ metadata: {
3705
+ ownership: fullGuild.ownerId ? { ownerId } : void 0,
3706
+ roles: {
3707
+ [ownerId]: Role.OWNER
3708
+ }
3709
+ }
3710
+ },
3711
+ source: "discord"
3712
+ };
3713
+ this.runtime.emitEvent(["DISCORD_WORLD_JOINED" /* WORLD_JOINED */], {
3714
+ runtime: this.runtime,
3715
+ server: fullGuild,
3716
+ source: "discord"
3717
+ });
3718
+ this.runtime.emitEvent([EventType2.WORLD_JOINED], standardizedData);
3719
+ }
3720
+ /**
3721
+ * Handles interactions created by the user, specifically commands and message components.
3722
+ * @param {Interaction} interaction - The interaction object received.
3723
+ * @returns {Promise<void>} A promise that resolves when the interaction is handled.
3724
+ * @private
3725
+ */
3726
+ async handleInteractionCreate(interaction) {
3727
+ if (interaction.isCommand()) {
3728
+ switch (interaction.commandName) {
3729
+ case "joinchannel":
3730
+ await this.voiceManager?.handleJoinChannelCommand(interaction);
3731
+ break;
3732
+ case "leavechannel":
3733
+ await this.voiceManager?.handleLeaveChannelCommand(interaction);
3734
+ break;
3735
+ }
3346
3736
  }
3347
- const maxRetries = 5;
3348
- let retryCount = 0;
3349
- let lastError = null;
3350
- while (retryCount < maxRetries) {
3737
+ if (interaction.isMessageComponent()) {
3738
+ logger6.info(`Received component interaction: ${interaction.customId}`);
3739
+ const userId = interaction.user?.id;
3740
+ const messageId = interaction.message?.id;
3741
+ if (!this.userSelections.has(userId)) {
3742
+ this.userSelections.set(userId, {});
3743
+ }
3744
+ const userSelections = this.userSelections.get(userId);
3745
+ if (!userSelections) {
3746
+ logger6.error(`User selections map unexpectedly missing for user ${userId}`);
3747
+ return;
3748
+ }
3351
3749
  try {
3352
- const service = new _DiscordService(runtime);
3353
- if (!service.client) {
3354
- throw new Error("Failed to initialize Discord client");
3750
+ if (interaction.isStringSelectMenu()) {
3751
+ logger6.info(`Values selected: ${JSON.stringify(interaction.values)}`);
3752
+ logger6.info(
3753
+ `User ${userId} selected values for ${interaction.customId}: ${JSON.stringify(interaction.values)}`
3754
+ );
3755
+ userSelections[messageId] = {
3756
+ ...userSelections[messageId],
3757
+ [interaction.customId]: interaction.values
3758
+ };
3759
+ logger6.info(
3760
+ `Current selections for message ${messageId}: ${JSON.stringify(userSelections[messageId])}`
3761
+ );
3762
+ await interaction.deferUpdate();
3355
3763
  }
3356
- await new Promise((resolve, reject) => {
3357
- const timeout = setTimeout(() => {
3358
- reject(new Error("Discord client ready timeout"));
3359
- }, 3e4);
3360
- service.client?.once("ready", () => {
3361
- clearTimeout(timeout);
3362
- resolve();
3764
+ if (interaction.isButton()) {
3765
+ logger6.info("Button interaction detected");
3766
+ logger6.info(`Button pressed by user ${userId}: ${interaction.customId}`);
3767
+ const formSelections = userSelections[messageId] || {};
3768
+ logger6.info(`Form data being submitted: ${JSON.stringify(formSelections)}`);
3769
+ this.runtime.emitEvent(["DISCORD_INTERACTION"], {
3770
+ interaction: {
3771
+ customId: interaction.customId,
3772
+ componentType: interaction.componentType,
3773
+ type: interaction.type,
3774
+ user: userId,
3775
+ messageId,
3776
+ selections: formSelections
3777
+ },
3778
+ source: "discord"
3363
3779
  });
3364
- });
3365
- return service;
3780
+ delete userSelections[messageId];
3781
+ logger6.info(`Cleared selections for message ${messageId}`);
3782
+ await interaction.deferUpdate();
3783
+ await interaction.followUp({
3784
+ content: "Form submitted successfully!",
3785
+ ephemeral: true
3786
+ });
3787
+ }
3366
3788
  } catch (error) {
3367
- lastError = error instanceof Error ? error : new Error(String(error));
3368
- logger6.error(
3369
- `Discord initialization attempt ${retryCount + 1} failed: ${lastError.message}`
3370
- );
3371
- retryCount++;
3372
- if (retryCount < maxRetries) {
3373
- const delay = 2 ** retryCount * 1e3;
3374
- logger6.info(`Retrying Discord initialization in ${delay / 1e3} seconds...`);
3375
- await new Promise((resolve) => setTimeout(resolve, delay));
3789
+ logger6.error(`Error handling component interaction: ${error}`);
3790
+ try {
3791
+ await interaction.followUp({
3792
+ content: "There was an error processing your interaction.",
3793
+ ephemeral: true
3794
+ });
3795
+ } catch (followUpError) {
3796
+ logger6.error(`Error sending follow-up message: ${followUpError}`);
3376
3797
  }
3377
3798
  }
3378
3799
  }
3379
- throw new Error(
3380
- `Discord initialization failed after ${maxRetries} attempts. Last error: ${lastError?.message}`
3381
- );
3382
3800
  }
3383
3801
  /**
3384
- * Stops the Discord client associated with the given runtime.
3802
+ * Builds a standardized list of rooms from Discord guild channels.
3385
3803
  *
3386
- * @param {IAgentRuntime} runtime - The runtime associated with the Discord client.
3387
- * @returns {void}
3804
+ * @param {Guild} guild The guild to build rooms for.
3805
+ * @param {UUID} _worldId The ID of the world to associate with the rooms (currently unused in favor of direct channel to room mapping).
3806
+ * @returns {Promise<any[]>} An array of standardized room objects.
3807
+ * @private
3388
3808
  */
3389
- static async stop(runtime) {
3390
- const client = runtime.getService(DISCORD_SERVICE_NAME);
3391
- if (!client) {
3392
- logger6.error("DiscordService not found");
3393
- return;
3394
- }
3395
- try {
3396
- await client.stop();
3397
- } catch (e) {
3398
- logger6.error("client-discord instance stop err", e);
3809
+ async buildStandardizedRooms(guild, _worldId) {
3810
+ const rooms = [];
3811
+ for (const [channelId, channel] of guild.channels.cache) {
3812
+ if (channel.type === DiscordChannelType4.GuildText || channel.type === DiscordChannelType4.GuildVoice) {
3813
+ const roomId = createUniqueUuid6(this.runtime, channelId);
3814
+ let channelType;
3815
+ switch (channel.type) {
3816
+ case DiscordChannelType4.GuildText:
3817
+ channelType = ChannelType9.GROUP;
3818
+ break;
3819
+ case DiscordChannelType4.GuildVoice:
3820
+ channelType = ChannelType9.VOICE_GROUP;
3821
+ break;
3822
+ default:
3823
+ channelType = ChannelType9.GROUP;
3824
+ }
3825
+ let participants = [];
3826
+ if (guild.memberCount < 1e3 && channel.type === DiscordChannelType4.GuildText) {
3827
+ try {
3828
+ participants = Array.from(guild.members.cache.values()).filter(
3829
+ (member) => channel.permissionsFor(member)?.has(PermissionsBitField2.Flags.ViewChannel)
3830
+ ).map((member) => createUniqueUuid6(this.runtime, member.id));
3831
+ } catch (error) {
3832
+ logger6.warn(`Failed to get participants for channel ${channel.name}:`, error);
3833
+ }
3834
+ }
3835
+ rooms.push({
3836
+ id: roomId,
3837
+ name: channel.name,
3838
+ type: channelType,
3839
+ channelId: channel.id,
3840
+ participants
3841
+ });
3842
+ }
3399
3843
  }
3844
+ return rooms;
3400
3845
  }
3401
3846
  /**
3402
- * Asynchronously stops the client by destroying it.
3847
+ * Builds a standardized list of users (entities) from Discord guild members.
3848
+ * Implements different strategies based on guild size for performance.
3403
3849
  *
3404
- * @returns {Promise<void>}
3405
- */
3406
- async stop() {
3407
- await this.client?.destroy();
3408
- }
3409
- /**
3410
- * Handle the event when the client is ready.
3411
- * @param {Object} readyClient - The ready client object containing user information.
3412
- * @param {string} readyClient.user.tag - The username and discriminator of the client user.
3413
- * @param {string} readyClient.user.id - The user ID of the client.
3414
- * @returns {Promise<void>}
3850
+ * @param {Guild} guild - The guild from which to build the user list.
3851
+ * @returns {Promise<Entity[]>} A promise that resolves with an array of standardized entity objects.
3852
+ * @private
3415
3853
  */
3416
- async onClientReady(readyClient) {
3417
- logger6.success(`DISCORD: Logged in as ${readyClient.user?.tag}`);
3418
- const commands = [
3419
- {
3420
- name: "joinchannel",
3421
- description: "Join a voice channel",
3422
- options: [
3423
- {
3424
- name: "channel",
3425
- type: 7,
3426
- // CHANNEL type
3427
- description: "The voice channel to join",
3428
- required: true,
3429
- channel_types: [2]
3430
- // GuildVoice type
3854
+ async buildStandardizedUsers(guild) {
3855
+ const entities = [];
3856
+ const botId = this.client?.user?.id;
3857
+ if (guild.memberCount > 1e3) {
3858
+ logger6.info(
3859
+ `Using optimized user sync for large guild ${guild.name} (${guild.memberCount} members)`
3860
+ );
3861
+ try {
3862
+ for (const [, member] of guild.members.cache) {
3863
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3864
+ if (member.id !== botId) {
3865
+ entities.push({
3866
+ id: createUniqueUuid6(this.runtime, member.id),
3867
+ names: Array.from(
3868
+ new Set(
3869
+ [member.user.username, member.displayName, member.user.globalName].filter(
3870
+ Boolean
3871
+ )
3872
+ )
3873
+ ),
3874
+ agentId: this.runtime.agentId,
3875
+ metadata: {
3876
+ default: {
3877
+ username: tag,
3878
+ name: member.displayName || member.user.username
3879
+ },
3880
+ discord: member.user.globalName ? {
3881
+ username: tag,
3882
+ name: member.displayName || member.user.username,
3883
+ globalName: member.user.globalName,
3884
+ userId: member.id
3885
+ } : {
3886
+ username: tag,
3887
+ name: member.displayName || member.user.username,
3888
+ userId: member.id
3889
+ }
3890
+ }
3891
+ });
3431
3892
  }
3432
- ]
3433
- },
3434
- {
3435
- name: "leavechannel",
3436
- description: "Leave the current voice channel"
3893
+ }
3894
+ if (entities.length < 100) {
3895
+ logger6.info(`Adding online members for ${guild.name}`);
3896
+ const onlineMembers = await guild.members.fetch({ limit: 100 });
3897
+ for (const [, member] of onlineMembers) {
3898
+ if (member.id !== botId) {
3899
+ const entityId = createUniqueUuid6(this.runtime, member.id);
3900
+ if (!entities.some((u) => u.id === entityId)) {
3901
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3902
+ entities.push({
3903
+ id: entityId,
3904
+ names: Array.from(
3905
+ new Set(
3906
+ [member.user.username, member.displayName, member.user.globalName].filter(
3907
+ Boolean
3908
+ )
3909
+ )
3910
+ ),
3911
+ agentId: this.runtime.agentId,
3912
+ metadata: {
3913
+ default: {
3914
+ username: tag,
3915
+ name: member.displayName || member.user.username
3916
+ },
3917
+ discord: member.user.globalName ? {
3918
+ username: tag,
3919
+ name: member.displayName || member.user.username,
3920
+ globalName: member.user.globalName,
3921
+ userId: member.id
3922
+ } : {
3923
+ username: tag,
3924
+ name: member.displayName || member.user.username,
3925
+ userId: member.id
3926
+ }
3927
+ }
3928
+ });
3929
+ }
3930
+ }
3931
+ }
3932
+ }
3933
+ } catch (error) {
3934
+ logger6.error(`Error fetching members for ${guild.name}:`, error);
3437
3935
  }
3438
- ];
3439
- try {
3440
- await this.client?.application?.commands.set(commands);
3441
- logger6.success("DISCORD: Slash commands registered");
3442
- } catch (error) {
3443
- console.error("Error registering slash commands:", error);
3444
- }
3445
- const requiredPermissions = [
3446
- // Text Permissions
3447
- PermissionsBitField2.Flags.ViewChannel,
3448
- PermissionsBitField2.Flags.SendMessages,
3449
- PermissionsBitField2.Flags.SendMessagesInThreads,
3450
- PermissionsBitField2.Flags.CreatePrivateThreads,
3451
- PermissionsBitField2.Flags.CreatePublicThreads,
3452
- PermissionsBitField2.Flags.EmbedLinks,
3453
- PermissionsBitField2.Flags.AttachFiles,
3454
- PermissionsBitField2.Flags.AddReactions,
3455
- PermissionsBitField2.Flags.UseExternalEmojis,
3456
- PermissionsBitField2.Flags.UseExternalStickers,
3457
- PermissionsBitField2.Flags.MentionEveryone,
3458
- PermissionsBitField2.Flags.ManageMessages,
3459
- PermissionsBitField2.Flags.ReadMessageHistory,
3460
- // Voice Permissions
3461
- PermissionsBitField2.Flags.Connect,
3462
- PermissionsBitField2.Flags.Speak,
3463
- PermissionsBitField2.Flags.UseVAD,
3464
- PermissionsBitField2.Flags.PrioritySpeaker
3465
- ].reduce((a, b) => a | b, 0n);
3466
- logger6.success("Use this URL to add the bot to your server:");
3467
- logger6.success(
3468
- `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user?.id}&permissions=${requiredPermissions}&scope=bot%20applications.commands`
3469
- );
3470
- await this.onReady();
3936
+ } else {
3937
+ try {
3938
+ let members = guild.members.cache;
3939
+ if (members.size === 0) {
3940
+ members = await guild.members.fetch();
3941
+ }
3942
+ for (const [, member] of members) {
3943
+ if (member.id !== botId) {
3944
+ const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3945
+ entities.push({
3946
+ id: createUniqueUuid6(this.runtime, member.id),
3947
+ names: Array.from(
3948
+ new Set(
3949
+ [member.user.username, member.displayName, member.user.globalName].filter(
3950
+ Boolean
3951
+ )
3952
+ )
3953
+ ),
3954
+ agentId: this.runtime.agentId,
3955
+ metadata: {
3956
+ default: {
3957
+ username: tag,
3958
+ name: member.displayName || member.user.username
3959
+ },
3960
+ discord: member.user.globalName ? {
3961
+ username: tag,
3962
+ name: member.displayName || member.user.username,
3963
+ globalName: member.user.globalName,
3964
+ userId: member.id
3965
+ } : {
3966
+ username: tag,
3967
+ name: member.displayName || member.user.username,
3968
+ userId: member.id
3969
+ }
3970
+ }
3971
+ });
3972
+ }
3973
+ }
3974
+ } catch (error) {
3975
+ logger6.error(`Error fetching members for ${guild.name}:`, error);
3976
+ }
3977
+ }
3978
+ return entities;
3471
3979
  }
3472
3980
  /**
3473
- * Asynchronously retrieves the type of a given channel.
3474
- *
3475
- * @param {Channel} channel - The channel for which to determine the type.
3476
- * @returns {Promise<ChannelType>} A Promise that resolves with the type of the channel.
3981
+ * Handles tasks to be performed once the Discord client is fully ready and connected.
3982
+ * This includes fetching guilds, scanning for voice data, and emitting connection events.
3983
+ * @private
3984
+ * @returns {Promise<void>} A promise that resolves when all on-ready tasks are completed.
3477
3985
  */
3478
- async getChannelType(channel) {
3479
- switch (channel.type) {
3480
- case DiscordChannelType4.DM:
3481
- return ChannelType9.DM;
3482
- case DiscordChannelType4.GuildText:
3483
- return ChannelType9.GROUP;
3484
- case DiscordChannelType4.GuildVoice:
3485
- return ChannelType9.VOICE_GROUP;
3986
+ async onReady() {
3987
+ logger6.log("DISCORD ON READY");
3988
+ const guilds = await this.client?.guilds.fetch();
3989
+ if (!guilds) {
3990
+ logger6.warn("Could not fetch guilds, client might not be ready.");
3991
+ return;
3992
+ }
3993
+ for (const [, guild] of guilds) {
3994
+ const fullGuild = await guild.fetch();
3995
+ await this.voiceManager?.scanGuild(fullGuild);
3996
+ const timeoutId = setTimeout(async () => {
3997
+ try {
3998
+ const fullGuild2 = await guild.fetch();
3999
+ logger6.log("DISCORD SERVER CONNECTED", fullGuild2.name);
4000
+ this.runtime.emitEvent(["DISCORD_SERVER_CONNECTED" /* WORLD_CONNECTED */], {
4001
+ runtime: this.runtime,
4002
+ server: fullGuild2,
4003
+ source: "discord"
4004
+ });
4005
+ const worldId = createUniqueUuid6(this.runtime, fullGuild2.id);
4006
+ const ownerId = createUniqueUuid6(this.runtime, fullGuild2.ownerId);
4007
+ const standardizedData = {
4008
+ name: fullGuild2.name,
4009
+ runtime: this.runtime,
4010
+ rooms: await this.buildStandardizedRooms(fullGuild2, worldId),
4011
+ entities: await this.buildStandardizedUsers(fullGuild2),
4012
+ world: {
4013
+ id: worldId,
4014
+ name: fullGuild2.name,
4015
+ agentId: this.runtime.agentId,
4016
+ serverId: fullGuild2.id,
4017
+ metadata: {
4018
+ ownership: fullGuild2.ownerId ? { ownerId } : void 0,
4019
+ roles: {
4020
+ [ownerId]: Role.OWNER
4021
+ }
4022
+ }
4023
+ },
4024
+ source: "discord"
4025
+ };
4026
+ this.runtime.emitEvent([EventType2.WORLD_CONNECTED], standardizedData);
4027
+ } catch (error) {
4028
+ logger6.error("Error during Discord world connection:", error);
4029
+ }
4030
+ }, 1e3);
4031
+ this.timeouts.push(timeoutId);
3486
4032
  }
4033
+ this.client?.emit("voiceManagerReady");
3487
4034
  }
3488
4035
  /**
3489
- * Handles the addition of a reaction on a message.
4036
+ * Registers send handlers for the Discord service instance.
4037
+ * This allows the runtime to correctly dispatch messages to this service.
4038
+ * @param {IAgentRuntime} runtime - The agent runtime instance.
4039
+ * @param {DiscordService} serviceInstance - The instance of the DiscordService.
4040
+ * @static
4041
+ */
4042
+ static registerSendHandlers(runtime, serviceInstance) {
4043
+ if (serviceInstance) {
4044
+ runtime.registerSendHandler(
4045
+ "discord",
4046
+ serviceInstance.handleSendMessage.bind(serviceInstance)
4047
+ );
4048
+ logger6.info("[Discord] Registered send handler.");
4049
+ }
4050
+ }
4051
+ /**
4052
+ * Fetches all members who have access to a specific text channel.
3490
4053
  *
3491
- * @param {MessageReaction | PartialMessageReaction} reaction The reaction that was added.
3492
- * @param {User | PartialUser} user The user who added the reaction.
3493
- * @returns {void}
4054
+ * @param {string} channelId - The Discord ID of the text channel.
4055
+ * @param {boolean} [useCache=true] - Whether to prioritize cached data. Defaults to true.
4056
+ * @returns {Promise<Array<{id: string, username: string, displayName: string}>>} A promise that resolves with an array of channel member objects, each containing id, username, and displayName.
4057
+ */
4058
+ async getTextChannelMembers(channelId, useCache = true) {
4059
+ logger6.info(`Fetching members for text channel ${channelId}, useCache=${useCache}`);
4060
+ try {
4061
+ const channel = await this.client?.channels.fetch(channelId);
4062
+ if (!channel) {
4063
+ logger6.error(`Channel not found: ${channelId}`);
4064
+ return [];
4065
+ }
4066
+ if (channel.type !== DiscordChannelType4.GuildText) {
4067
+ logger6.error(`Channel ${channelId} is not a text channel`);
4068
+ return [];
4069
+ }
4070
+ const guild = channel.guild;
4071
+ if (!guild) {
4072
+ logger6.error(`Channel ${channelId} is not in a guild`);
4073
+ return [];
4074
+ }
4075
+ const useCacheOnly = useCache && guild.memberCount > 1e3;
4076
+ let members;
4077
+ if (useCacheOnly) {
4078
+ logger6.info(
4079
+ `Using cached members for large guild ${guild.name} (${guild.memberCount} members)`
4080
+ );
4081
+ members = guild.members.cache;
4082
+ } else {
4083
+ try {
4084
+ if (useCache && guild.members.cache.size > 0) {
4085
+ logger6.info(`Using cached members (${guild.members.cache.size} members)`);
4086
+ members = guild.members.cache;
4087
+ } else {
4088
+ logger6.info(`Fetching members for guild ${guild.name}`);
4089
+ members = await guild.members.fetch();
4090
+ logger6.info(`Fetched ${members.size} members`);
4091
+ }
4092
+ } catch (error) {
4093
+ logger6.error(`Error fetching members: ${error}`);
4094
+ members = guild.members.cache;
4095
+ logger6.info(`Fallback to cache with ${members.size} members`);
4096
+ }
4097
+ }
4098
+ logger6.info(`Filtering members for access to channel ${channel.name}`);
4099
+ const memberArray = Array.from(members.values());
4100
+ const channelMembers = memberArray.filter((member) => {
4101
+ if (member.user.bot && member.id !== this.client?.user?.id) {
4102
+ return false;
4103
+ }
4104
+ return channel.permissionsFor(member)?.has(PermissionsBitField2.Flags.ViewChannel) ?? false;
4105
+ }).map((member) => ({
4106
+ id: member.id,
4107
+ username: member.user.username,
4108
+ displayName: member.displayName || member.user.username
4109
+ }));
4110
+ logger6.info(`Found ${channelMembers.length} members with access to channel ${channel.name}`);
4111
+ return channelMembers;
4112
+ } catch (error) {
4113
+ logger6.error(`Error fetching channel members: ${error}`);
4114
+ return [];
4115
+ }
4116
+ }
4117
+ /**
4118
+ * Placeholder for handling reaction addition.
4119
+ * @private
3494
4120
  */
3495
4121
  async handleReactionAdd(reaction, user) {
3496
4122
  try {
@@ -3527,13 +4153,15 @@ var DiscordService = class _DiscordService extends Service {
3527
4153
  }
3528
4154
  const messageContent = reaction.message.content || "";
3529
4155
  const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3530
- const reactionMessage = `*Added <${emoji}> to: "${truncatedContent}"*`;
4156
+ const reactionMessage = `*Added <${emoji}> to: \\"${truncatedContent}\\"*`;
3531
4157
  const userName = reaction.message.author?.username || "unknown";
3532
4158
  const name = reaction.message.author?.displayName || userName;
3533
4159
  await this.runtime.ensureConnection({
3534
4160
  entityId,
3535
4161
  roomId,
3536
4162
  userName,
4163
+ worldId: createUniqueUuid6(this.runtime, reaction.message.guild?.id ?? roomId),
4164
+ worldName: reaction.message.guild?.name,
3537
4165
  name,
3538
4166
  source: "discord",
3539
4167
  channelId: reaction.message.channel.id,
@@ -3559,9 +4187,9 @@ var DiscordService = class _DiscordService extends Service {
3559
4187
  const callback = async (content) => {
3560
4188
  if (!reaction.message.channel) {
3561
4189
  logger6.error("No channel found for reaction message");
3562
- return;
4190
+ return [];
3563
4191
  }
3564
- await reaction.message.channel.send(content.text);
4192
+ await reaction.message.channel.send(content.text ?? "");
3565
4193
  return [];
3566
4194
  };
3567
4195
  this.runtime.emitEvent(["DISCORD_REACTION_RECEIVED", "REACTION_RECEIVED"], {
@@ -3574,11 +4202,8 @@ var DiscordService = class _DiscordService extends Service {
3574
4202
  }
3575
4203
  }
3576
4204
  /**
3577
- * Handles the removal of a reaction on a message.
3578
- *
3579
- * @param {MessageReaction | PartialMessageReaction} reaction - The reaction that was removed.
3580
- * @param {User | PartialUser} user - The user who removed the reaction.
3581
- * @returns {Promise<void>} - A Promise that resolves after handling the reaction removal.
4205
+ * Placeholder for handling reaction removal.
4206
+ * @private
3582
4207
  */
3583
4208
  async handleReactionRemove(reaction, user) {
3584
4209
  try {
@@ -3597,7 +4222,7 @@ var DiscordService = class _DiscordService extends Service {
3597
4222
  }
3598
4223
  const messageContent = reaction.message.content || "";
3599
4224
  const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3600
- const reactionMessage = `*Removed <${emoji}> from: "${truncatedContent}"*`;
4225
+ const reactionMessage = `*Removed <${emoji}> from: \\"${truncatedContent}\\"*`;
3601
4226
  const roomId = createUniqueUuid6(this.runtime, reaction.message.channel.id);
3602
4227
  const entityId = createUniqueUuid6(this.runtime, user.id);
3603
4228
  const timestamp = Date.now();
@@ -3611,6 +4236,8 @@ var DiscordService = class _DiscordService extends Service {
3611
4236
  entityId,
3612
4237
  roomId,
3613
4238
  userName,
4239
+ worldId: createUniqueUuid6(this.runtime, reaction.message.guild?.id ?? roomId),
4240
+ worldName: reaction.message.guild?.name,
3614
4241
  name,
3615
4242
  source: "discord",
3616
4243
  channelId: reaction.message.channel.id,
@@ -3635,9 +4262,9 @@ var DiscordService = class _DiscordService extends Service {
3635
4262
  const callback = async (content) => {
3636
4263
  if (!reaction.message.channel) {
3637
4264
  logger6.error("No channel found for reaction message");
3638
- return;
4265
+ return [];
3639
4266
  }
3640
- await reaction.message.channel.send(content.text);
4267
+ await reaction.message.channel.send(content.text ?? "");
3641
4268
  return [];
3642
4269
  };
3643
4270
  this.runtime.emitEvent(["DISCORD_REACTION_RECEIVED" /* REACTION_RECEIVED */], {
@@ -3650,260 +4277,40 @@ var DiscordService = class _DiscordService extends Service {
3650
4277
  }
3651
4278
  }
3652
4279
  /**
3653
- * Handles the event when the bot joins a guild. It logs the guild name, fetches additional information about the guild, scans the guild for voice data, creates standardized world data structure, generates unique IDs, and emits events to the runtime.
3654
- * @param {Guild} guild - The guild that the bot has joined.
3655
- * @returns {Promise<void>}
4280
+ * Stops the Discord service and cleans up resources.
4281
+ * Implements the abstract method from the Service class.
3656
4282
  */
3657
- async handleGuildCreate(guild) {
3658
- logger6.log(`Joined guild ${guild.name}`);
3659
- const fullGuild = await guild.fetch();
3660
- this.voiceManager.scanGuild(guild);
3661
- const ownerId = createUniqueUuid6(this.runtime, fullGuild.ownerId);
3662
- const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
3663
- const standardizedData = {
3664
- runtime: this.runtime,
3665
- rooms: await this.buildStandardizedRooms(fullGuild, worldId),
3666
- users: await this.buildStandardizedUsers(fullGuild),
3667
- world: {
3668
- id: worldId,
3669
- name: fullGuild.name,
3670
- agentId: this.runtime.agentId,
3671
- serverId: fullGuild.id,
3672
- metadata: {
3673
- ownership: fullGuild.ownerId ? { ownerId } : void 0,
3674
- roles: {
3675
- [ownerId]: Role.OWNER
3676
- }
3677
- }
3678
- },
3679
- source: "discord"
3680
- };
3681
- this.runtime.emitEvent(["DISCORD_WORLD_JOINED" /* WORLD_JOINED */], {
3682
- runtime: this.runtime,
3683
- server: fullGuild,
3684
- source: "discord"
3685
- });
3686
- this.runtime.emitEvent([EventType2.WORLD_JOINED], standardizedData);
3687
- }
3688
- /**
3689
- * Handles interactions created by the user, specifically commands.
3690
- * @param {any} interaction - The interaction object received
3691
- * @returns {void}
3692
- */
3693
- async handleInteractionCreate(interaction) {
3694
- if (!interaction.isCommand()) return;
3695
- switch (interaction.commandName) {
3696
- case "joinchannel":
3697
- await this.voiceManager.handleJoinChannelCommand(interaction);
3698
- break;
3699
- case "leavechannel":
3700
- await this.voiceManager.handleLeaveChannelCommand(interaction);
3701
- break;
4283
+ async stop() {
4284
+ logger6.info("Stopping Discord service...");
4285
+ this.timeouts.forEach(clearTimeout);
4286
+ this.timeouts = [];
4287
+ if (this.client) {
4288
+ await this.client.destroy();
4289
+ this.client = null;
4290
+ logger6.info("Discord client destroyed.");
3702
4291
  }
3703
- }
3704
- /**
3705
- * Builds a standardized list of rooms from Discord guild channels
3706
- */
3707
- /**
3708
- * Build standardized rooms for a guild based on text and voice channels.
3709
- *
3710
- * @param {Guild} guild The guild to build rooms for.
3711
- * @param {UUID} _worldId The ID of the world to associate with the rooms.
3712
- * @returns {Promise<any[]>} An array of standardized room objects.
3713
- */
3714
- async buildStandardizedRooms(guild, _worldId) {
3715
- const rooms = [];
3716
- for (const [channelId, channel] of guild.channels.cache) {
3717
- if (channel.type === DiscordChannelType4.GuildText || channel.type === DiscordChannelType4.GuildVoice) {
3718
- const roomId = createUniqueUuid6(this.runtime, channelId);
3719
- let channelType;
3720
- switch (channel.type) {
3721
- case DiscordChannelType4.GuildText:
3722
- channelType = ChannelType9.GROUP;
3723
- break;
3724
- case DiscordChannelType4.GuildVoice:
3725
- channelType = ChannelType9.VOICE_GROUP;
3726
- break;
3727
- default:
3728
- channelType = ChannelType9.GROUP;
3729
- }
3730
- let participants = [];
3731
- if (guild.memberCount < 1e3 && channel.type === DiscordChannelType4.GuildText) {
3732
- try {
3733
- participants = Array.from(guild.members.cache.values()).filter(
3734
- (member) => channel.permissionsFor(member)?.has(PermissionsBitField2.Flags.ViewChannel)
3735
- ).map((member) => createUniqueUuid6(this.runtime, member.id));
3736
- } catch (error) {
3737
- logger6.warn(`Failed to get participants for channel ${channel.name}:`, error);
3738
- }
3739
- }
3740
- rooms.push({
3741
- id: roomId,
3742
- name: channel.name,
3743
- type: channelType,
3744
- channelId: channel.id,
3745
- participants
3746
- });
3747
- }
4292
+ if (this.voiceManager) {
3748
4293
  }
3749
- return rooms;
4294
+ logger6.info("Discord service stopped.");
3750
4295
  }
3751
4296
  /**
3752
- * Builds a standardized list of users from Discord guild members
4297
+ * Asynchronously retrieves the type of a given channel.
4298
+ *
4299
+ * @param {Channel} channel - The channel for which to determine the type.
4300
+ * @returns {Promise<ChannelType>} A Promise that resolves with the type of the channel.
3753
4301
  */
3754
- async buildStandardizedUsers(guild) {
3755
- const entities = [];
3756
- const botId = this.client?.user?.id;
3757
- if (guild.memberCount > 1e3) {
3758
- logger6.info(
3759
- `Using optimized user sync for large guild ${guild.name} (${guild.memberCount} members)`
3760
- );
3761
- try {
3762
- for (const [, member] of guild.members.cache) {
3763
- const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3764
- if (member.id !== botId) {
3765
- entities.push({
3766
- id: createUniqueUuid6(this.runtime, member.id),
3767
- names: Array.from(
3768
- /* @__PURE__ */ new Set([member.user.username, member.displayName, member.user.globalName])
3769
- ),
3770
- agentId: this.runtime.agentId,
3771
- metadata: {
3772
- default: {
3773
- username: tag,
3774
- name: member.displayName || member.user.username
3775
- },
3776
- discord: member.user.globalName ? {
3777
- username: tag,
3778
- name: member.displayName || member.user.username,
3779
- globalName: member.user.globalName,
3780
- userId: member.id
3781
- } : {
3782
- username: tag,
3783
- name: member.displayName || member.user.username,
3784
- userId: member.id
3785
- }
3786
- }
3787
- });
3788
- }
3789
- }
3790
- if (entities.length < 100) {
3791
- logger6.info(`Adding online members for ${guild.name}`);
3792
- const onlineMembers = await guild.members.fetch({ limit: 100 });
3793
- for (const [, member] of onlineMembers) {
3794
- if (member.id !== botId) {
3795
- const entityId = createUniqueUuid6(this.runtime, member.id);
3796
- if (!entities.some((u) => u.id === entityId)) {
3797
- const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3798
- entities.push({
3799
- id: entityId,
3800
- names: Array.from(
3801
- /* @__PURE__ */ new Set([member.user.username, member.displayName, member.user.globalName])
3802
- ),
3803
- agentId: this.runtime.agentId,
3804
- metadata: {
3805
- default: {
3806
- username: tag,
3807
- name: member.displayName || member.user.username
3808
- },
3809
- discord: member.user.globalName ? {
3810
- username: tag,
3811
- name: member.displayName || member.user.username,
3812
- globalName: member.user.globalName,
3813
- userId: member.id
3814
- } : {
3815
- username: tag,
3816
- name: member.displayName || member.user.username,
3817
- userId: member.id
3818
- }
3819
- }
3820
- });
3821
- }
3822
- }
3823
- }
3824
- }
3825
- } catch (error) {
3826
- logger6.error(`Error fetching members for ${guild.name}:`, error);
3827
- }
3828
- } else {
3829
- try {
3830
- let members = guild.members.cache;
3831
- if (members.size === 0) {
3832
- members = await guild.members.fetch();
3833
- }
3834
- for (const [, member] of members) {
3835
- if (member.id !== botId) {
3836
- const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3837
- entities.push({
3838
- id: createUniqueUuid6(this.runtime, member.id),
3839
- names: Array.from(
3840
- /* @__PURE__ */ new Set([member.user.username, member.displayName, member.user.globalName])
3841
- ),
3842
- agentId: this.runtime.agentId,
3843
- metadata: {
3844
- default: {
3845
- username: tag,
3846
- name: member.displayName || member.user.username
3847
- },
3848
- discord: member.user.globalName ? {
3849
- username: tag,
3850
- name: member.displayName || member.user.username,
3851
- globalName: member.user.globalName,
3852
- userId: member.id
3853
- } : {
3854
- username: tag,
3855
- name: member.displayName || member.user.username,
3856
- userId: member.id
3857
- }
3858
- }
3859
- });
3860
- }
3861
- }
3862
- } catch (error) {
3863
- logger6.error(`Error fetching members for ${guild.name}:`, error);
3864
- }
3865
- }
3866
- return entities;
3867
- }
3868
- async onReady() {
3869
- logger6.log("DISCORD ON READY");
3870
- const guilds = await this.client?.guilds.fetch();
3871
- for (const [, guild] of guilds) {
3872
- const fullGuild = await guild.fetch();
3873
- await this.voiceManager.scanGuild(fullGuild);
3874
- setTimeout(async () => {
3875
- const fullGuild2 = await guild.fetch();
3876
- logger6.log("DISCORD SERVER CONNECTED", fullGuild2.name);
3877
- this.runtime.emitEvent(["DISCORD_SERVER_CONNECTED" /* WORLD_CONNECTED */], {
3878
- runtime: this.runtime,
3879
- server: fullGuild2,
3880
- source: "discord"
3881
- });
3882
- const worldId = createUniqueUuid6(this.runtime, fullGuild2.id);
3883
- const ownerId = createUniqueUuid6(this.runtime, fullGuild2.ownerId);
3884
- const standardizedData = {
3885
- name: fullGuild2.name,
3886
- runtime: this.runtime,
3887
- rooms: await this.buildStandardizedRooms(fullGuild2, worldId),
3888
- entities: await this.buildStandardizedUsers(fullGuild2),
3889
- world: {
3890
- id: worldId,
3891
- name: fullGuild2.name,
3892
- agentId: this.runtime.agentId,
3893
- serverId: fullGuild2.id,
3894
- metadata: {
3895
- ownership: fullGuild2.ownerId ? { ownerId } : void 0,
3896
- roles: {
3897
- [ownerId]: Role.OWNER
3898
- }
3899
- }
3900
- },
3901
- source: "discord"
3902
- };
3903
- this.runtime.emitEvent([EventType2.WORLD_CONNECTED], standardizedData);
3904
- }, 1e3);
4302
+ async getChannelType(channel) {
4303
+ switch (channel.type) {
4304
+ case DiscordChannelType4.DM:
4305
+ return ChannelType9.DM;
4306
+ case DiscordChannelType4.GuildText:
4307
+ return ChannelType9.GROUP;
4308
+ case DiscordChannelType4.GuildVoice:
4309
+ return ChannelType9.VOICE_GROUP;
4310
+ default:
4311
+ logger6.warn(`Unhandled channel type: ${channel.type}`);
4312
+ return ChannelType9.GROUP;
3905
4313
  }
3906
- this.client?.emit("voiceManagerReady");
3907
4314
  }
3908
4315
  };
3909
4316
 
@@ -3917,11 +4324,12 @@ import {
3917
4324
  entersState as entersState2
3918
4325
  } from "@discordjs/voice";
3919
4326
  import { ModelType as ModelType9, logger as logger7 } from "@elizaos/core";
3920
- import { ChannelType as ChannelType10, Events as Events2 } from "discord.js";
4327
+ import { ChannelType as ChannelType10, Events as Events2, AttachmentBuilder } from "discord.js";
3921
4328
  var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
3922
4329
  var DiscordTestSuite = class {
3923
4330
  name = "discord";
3924
- discordClient = null;
4331
+ discordClient;
4332
+ // Use definite assignment assertion
3925
4333
  tests;
3926
4334
  /**
3927
4335
  * Constructor for initializing the tests array with test cases to be executed.
@@ -3967,13 +4375,19 @@ var DiscordTestSuite = class {
3967
4375
  async testCreatingDiscordClient(runtime) {
3968
4376
  try {
3969
4377
  this.discordClient = runtime.getService(ServiceType2.DISCORD);
3970
- if (this.discordClient.client.isReady()) {
4378
+ if (!this.discordClient) {
4379
+ throw new Error("Failed to get DiscordService from runtime.");
4380
+ }
4381
+ if (this.discordClient.client?.isReady()) {
3971
4382
  logger7.success("DiscordService is already ready.");
3972
4383
  } else {
3973
4384
  logger7.info("Waiting for DiscordService to be ready...");
4385
+ if (!this.discordClient.client) {
4386
+ throw new Error("Discord client instance is missing within the service.");
4387
+ }
3974
4388
  await new Promise((resolve, reject) => {
3975
- this.discordClient.client.once(Events2.ClientReady, resolve);
3976
- this.discordClient.client.once(Events2.Error, reject);
4389
+ this.discordClient.client?.once(Events2.ClientReady, resolve);
4390
+ this.discordClient.client?.once(Events2.Error, reject);
3977
4391
  });
3978
4392
  }
3979
4393
  } catch (error) {
@@ -3988,6 +4402,7 @@ var DiscordTestSuite = class {
3988
4402
  * @throws {Error} - If there is an error in executing the slash command test.
3989
4403
  */
3990
4404
  async testJoinVoiceSlashCommand(runtime) {
4405
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
3991
4406
  try {
3992
4407
  await this.waitForVoiceManagerReady(this.discordClient);
3993
4408
  const channel = await this.getTestChannel(runtime);
@@ -4007,10 +4422,13 @@ var DiscordTestSuite = class {
4007
4422
  logger7.info(`JoinChannel Slash Command Response: ${message}`);
4008
4423
  }
4009
4424
  };
4425
+ if (!this.discordClient.voiceManager) {
4426
+ throw new Error("VoiceManager is not available on the Discord client.");
4427
+ }
4010
4428
  await this.discordClient.voiceManager.handleJoinChannelCommand(fakeJoinInteraction);
4011
- logger7.success("Slash command test completed successfully.");
4429
+ logger7.success("Join voice slash command test completed successfully.");
4012
4430
  } catch (error) {
4013
- throw new Error(`Error in slash commands test: ${error}`);
4431
+ throw new Error(`Error in join voice slash commands test: ${error}`);
4014
4432
  }
4015
4433
  }
4016
4434
  /**
@@ -4020,6 +4438,7 @@ var DiscordTestSuite = class {
4020
4438
  * @returns {Promise<void>} A promise that resolves when the test is complete.
4021
4439
  */
4022
4440
  async testLeaveVoiceSlashCommand(runtime) {
4441
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4023
4442
  try {
4024
4443
  await this.waitForVoiceManagerReady(this.discordClient);
4025
4444
  const channel = await this.getTestChannel(runtime);
@@ -4034,10 +4453,13 @@ var DiscordTestSuite = class {
4034
4453
  logger7.info(`LeaveChannel Slash Command Response: ${message}`);
4035
4454
  }
4036
4455
  };
4456
+ if (!this.discordClient.voiceManager) {
4457
+ throw new Error("VoiceManager is not available on the Discord client.");
4458
+ }
4037
4459
  await this.discordClient.voiceManager.handleLeaveChannelCommand(fakeLeaveInteraction);
4038
- logger7.success("Slash command test completed successfully.");
4460
+ logger7.success("Leave voice slash command test completed successfully.");
4039
4461
  } catch (error) {
4040
- throw new Error(`Error in slash commands test: ${error}`);
4462
+ throw new Error(`Error in leave voice slash commands test: ${error}`);
4041
4463
  }
4042
4464
  }
4043
4465
  /**
@@ -4046,16 +4468,26 @@ var DiscordTestSuite = class {
4046
4468
  * @throws {Error} - If voice channel is invalid, voice connection fails to become ready, or no text to speech service found.
4047
4469
  */
4048
4470
  async testTextToSpeechPlayback(runtime) {
4471
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4049
4472
  try {
4050
4473
  await this.waitForVoiceManagerReady(this.discordClient);
4051
4474
  const channel = await this.getTestChannel(runtime);
4052
4475
  if (!channel || channel.type !== ChannelType10.GuildVoice) {
4053
4476
  throw new Error("Invalid voice channel.");
4054
4477
  }
4478
+ if (!this.discordClient.voiceManager) {
4479
+ throw new Error("VoiceManager is not available on the Discord client.");
4480
+ }
4055
4481
  await this.discordClient.voiceManager.joinChannel(channel);
4056
4482
  const guild = await this.getActiveGuild(this.discordClient);
4057
4483
  const guildId = guild.id;
4484
+ if (!this.discordClient.voiceManager) {
4485
+ throw new Error("VoiceManager is not available on the Discord client.");
4486
+ }
4058
4487
  const connection = this.discordClient.voiceManager.getVoiceConnection(guildId);
4488
+ if (!connection) {
4489
+ throw new Error(`No voice connection found for guild: ${guildId}`);
4490
+ }
4059
4491
  try {
4060
4492
  await entersState2(connection, VoiceConnectionStatus2.Ready, 1e4);
4061
4493
  logger7.success(`Voice connection is ready in guild: ${guildId}`);
@@ -4087,9 +4519,14 @@ var DiscordTestSuite = class {
4087
4519
  * @throws {Error} If there is an error in sending the text message.
4088
4520
  */
4089
4521
  async testSendingTextMessage(runtime) {
4522
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4090
4523
  try {
4091
4524
  const channel = await this.getTestChannel(runtime);
4092
- await this.sendMessageToChannel(channel, "Testing Message", [TEST_IMAGE_URL]);
4525
+ if (!channel || !channel.isTextBased()) {
4526
+ throw new Error("Cannot send message to a non-text channel.");
4527
+ }
4528
+ const attachment = new AttachmentBuilder(TEST_IMAGE_URL);
4529
+ await this.sendMessageToChannel(channel, "Testing Message", [attachment]);
4093
4530
  } catch (error) {
4094
4531
  throw new Error(`Error in sending text message: ${error}`);
4095
4532
  }
@@ -4101,6 +4538,7 @@ var DiscordTestSuite = class {
4101
4538
  * @returns {Promise<void>} A Promise that resolves once the message is handled.
4102
4539
  */
4103
4540
  async testHandlingMessage(runtime) {
4541
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4104
4542
  try {
4105
4543
  const channel = await this.getTestChannel(runtime);
4106
4544
  const fakeMessage = {
@@ -4119,9 +4557,12 @@ var DiscordTestSuite = class {
4119
4557
  reference: null,
4120
4558
  attachments: []
4121
4559
  };
4560
+ if (!this.discordClient.messageManager) {
4561
+ throw new Error("MessageManager is not available on the Discord client.");
4562
+ }
4122
4563
  await this.discordClient.messageManager.handleMessage(fakeMessage);
4123
4564
  } catch (error) {
4124
- throw new Error(`Error in sending text message: ${error}`);
4565
+ throw new Error(`Error in handling message test: ${error}`);
4125
4566
  }
4126
4567
  }
4127
4568
  // #############################
@@ -4135,8 +4576,9 @@ var DiscordTestSuite = class {
4135
4576
  * @throws {Error} If no test channel is found.
4136
4577
  */
4137
4578
  async getTestChannel(runtime) {
4579
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4138
4580
  const channelId = this.validateChannelId(runtime);
4139
- const channel = await this.discordClient.client.channels.fetch(channelId);
4581
+ const channel = await this.discordClient.client?.channels.fetch(channelId);
4140
4582
  if (!channel) throw new Error("no test channel found!");
4141
4583
  return channel;
4142
4584
  }
@@ -4154,7 +4596,7 @@ var DiscordTestSuite = class {
4154
4596
  if (!channel || !channel.isTextBased()) {
4155
4597
  throw new Error("Channel is not a text-based channel or does not exist.");
4156
4598
  }
4157
- await sendMessageInChunks(channel, messageContent, null, files);
4599
+ await sendMessageInChunks(channel, messageContent, "", files);
4158
4600
  } catch (error) {
4159
4601
  throw new Error(`Error sending message: ${error}`);
4160
4602
  }
@@ -4195,6 +4637,9 @@ var DiscordTestSuite = class {
4195
4637
  * @throws {Error} If no active voice connection is found for the bot.
4196
4638
  */
4197
4639
  async getActiveGuild(discordClient) {
4640
+ if (!discordClient.client) {
4641
+ throw new Error("Discord client instance is missing within the service.");
4642
+ }
4198
4643
  const guilds = await discordClient.client.guilds.fetch();
4199
4644
  const fullGuilds = await Promise.all(guilds.map((guild) => guild.fetch()));
4200
4645
  const activeGuild = fullGuilds.find((g) => g.members.me?.voice.channelId);
@@ -4214,10 +4659,13 @@ var DiscordTestSuite = class {
4214
4659
  if (!discordClient) {
4215
4660
  throw new Error("Discord client is not initialized.");
4216
4661
  }
4217
- if (!discordClient.voiceManager.isReady()) {
4662
+ if (!discordClient.voiceManager) {
4663
+ throw new Error("VoiceManager is not available on the Discord client.");
4664
+ }
4665
+ if (!discordClient.voiceManager?.isReady()) {
4218
4666
  await new Promise((resolve, reject) => {
4219
- discordClient.voiceManager.once("ready", resolve);
4220
- discordClient.voiceManager.once("error", reject);
4667
+ discordClient.voiceManager?.once("ready", resolve);
4668
+ discordClient.voiceManager?.once("error", reject);
4221
4669
  });
4222
4670
  }
4223
4671
  }