@elizaos/plugin-discord 1.0.0-beta.8 → 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,
@@ -2063,25 +2124,102 @@ import {
2063
2124
  ThreadChannel
2064
2125
  } from "discord.js";
2065
2126
  var MAX_MESSAGE_LENGTH = 1900;
2066
- async function sendMessageInChunks(channel, content, _inReplyTo, files) {
2127
+ async function sendMessageInChunks(channel, content, _inReplyTo, files, components) {
2067
2128
  const sentMessages = [];
2068
2129
  const messages = splitMessage(content);
2069
2130
  try {
2070
2131
  for (let i = 0; i < messages.length; i++) {
2071
2132
  const message = messages[i];
2072
- 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) {
2073
2134
  const options = {
2074
2135
  content: message.trim()
2075
2136
  };
2076
2137
  if (i === messages.length - 1 && files && files.length > 0) {
2077
2138
  options.files = files;
2078
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
+ }
2079
2217
  const m = await channel.send(options);
2080
2218
  sentMessages.push(m);
2081
2219
  }
2082
2220
  }
2083
2221
  } catch (error) {
2084
- logger3.error("Error sending message:", error);
2222
+ logger3.error(`Error sending message: ${error}`);
2085
2223
  }
2086
2224
  return sentMessages;
2087
2225
  }
@@ -2190,7 +2328,7 @@ var MessageManager = class {
2190
2328
  if (this.runtime.character.settings?.discord?.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType2.DM) {
2191
2329
  return;
2192
2330
  }
2193
- if (this.runtime.character.settings?.discord?.shouldRespondOnlyToMentions && !message.mentions.users?.has(this.client.user?.id)) {
2331
+ if (this.runtime.character.settings?.discord?.shouldRespondOnlyToMentions && (!this.client.user?.id || !message.mentions.users?.has(this.client.user.id))) {
2194
2332
  return;
2195
2333
  }
2196
2334
  const entityId = createUniqueUuid4(this.runtime, message.author.id);
@@ -2203,6 +2341,9 @@ var MessageManager = class {
2203
2341
  if (message.guild) {
2204
2342
  const guild = await message.guild.fetch();
2205
2343
  type = await this.getChannelType(message.channel);
2344
+ if (type === null) {
2345
+ logger4.warn("null channel type, discord message", message);
2346
+ }
2206
2347
  serverId = guild.id;
2207
2348
  } else {
2208
2349
  type = ChannelType7.DM;
@@ -2216,7 +2357,9 @@ var MessageManager = class {
2216
2357
  source: "discord",
2217
2358
  channelId: message.channel.id,
2218
2359
  serverId,
2219
- type
2360
+ type,
2361
+ worldId: createUniqueUuid4(this.runtime, serverId ?? roomId),
2362
+ worldName: message.guild?.name
2220
2363
  });
2221
2364
  try {
2222
2365
  const canSendResult = canSendMessage(message.channel);
@@ -2236,6 +2379,22 @@ var MessageManager = class {
2236
2379
  }
2237
2380
  const entityId2 = createUniqueUuid4(this.runtime, message.author.id);
2238
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
+ };
2239
2398
  const newMessage = {
2240
2399
  id: messageId,
2241
2400
  entityId: entityId2,
@@ -2250,6 +2409,10 @@ var MessageManager = class {
2250
2409
  url: message.url,
2251
2410
  inReplyTo: message.reference?.messageId ? createUniqueUuid4(this.runtime, message.reference?.messageId) : void 0
2252
2411
  },
2412
+ metadata: {
2413
+ entityName: name,
2414
+ type: "message"
2415
+ },
2253
2416
  createdAt: message.createdTimestamp
2254
2417
  };
2255
2418
  const callback = async (content, files) => {
@@ -2257,37 +2420,54 @@ var MessageManager = class {
2257
2420
  if (message.id && !content.inReplyTo) {
2258
2421
  content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
2259
2422
  }
2260
- const messages = await sendMessageInChunks(
2261
- message.channel,
2262
- content.text,
2263
- message.id,
2264
- files
2265
- );
2266
- const memories = [];
2267
- for (const m of messages) {
2268
- const actions = content.actions;
2269
- const memory = {
2270
- id: createUniqueUuid4(this.runtime, m.id),
2271
- entityId: this.runtime.agentId,
2272
- agentId: this.runtime.agentId,
2273
- content: {
2274
- ...content,
2275
- actions,
2276
- inReplyTo: messageId,
2277
- url: m.url,
2278
- channelType: type
2279
- },
2280
- roomId,
2281
- createdAt: m.createdTimestamp
2282
- };
2283
- memories.push(memory);
2284
- }
2285
- for (const m of memories) {
2286
- 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 [];
2287
2464
  }
2288
- return memories;
2289
2465
  } catch (error) {
2290
- 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
+ }
2291
2471
  return [];
2292
2472
  }
2293
2473
  };
@@ -2296,6 +2476,12 @@ var MessageManager = class {
2296
2476
  message: newMessage,
2297
2477
  callback
2298
2478
  });
2479
+ setTimeout(() => {
2480
+ if (typingData.interval && !typingData.cleared) {
2481
+ clearInterval(typingData.interval);
2482
+ typingData.cleared = true;
2483
+ }
2484
+ }, 500);
2299
2485
  } catch (error) {
2300
2486
  console.error("Error handling message:", error);
2301
2487
  }
@@ -2342,11 +2528,8 @@ var MessageManager = class {
2342
2528
  const urlRegex = /(https?:\/\/[^\s]+)/g;
2343
2529
  const urls = processedContent.match(urlRegex) || [];
2344
2530
  for (const url of urls) {
2345
- if (this.runtime.getService(ServiceType4.VIDEO)?.isVideoUrl(url)) {
2346
- const videoService = this.runtime.getService(ServiceType4.VIDEO);
2347
- if (!videoService) {
2348
- throw new Error("Video service not found");
2349
- }
2531
+ const videoService = this.runtime.getService(ServiceType4.VIDEO);
2532
+ if (videoService?.isVideoUrl(url)) {
2350
2533
  const videoInfo = await videoService.processVideo(url, this.runtime);
2351
2534
  attachments.push({
2352
2535
  id: `youtube-${Date.now()}`,
@@ -2359,7 +2542,8 @@ var MessageManager = class {
2359
2542
  } else {
2360
2543
  const browserService = this.runtime.getService(ServiceType4.BROWSER);
2361
2544
  if (!browserService) {
2362
- throw new Error("Browser service not found");
2545
+ logger4.warn("Browser service not found");
2546
+ continue;
2363
2547
  }
2364
2548
  const { title, description: summary } = await browserService.getPageContent(
2365
2549
  url,
@@ -2548,9 +2732,17 @@ var VoiceManager = class extends EventEmitter {
2548
2732
  super();
2549
2733
  this.client = service.client;
2550
2734
  this.runtime = runtime;
2551
- this.client.on("voiceManagerReady", () => {
2552
- this.setReady(true);
2553
- });
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
+ }
2554
2746
  }
2555
2747
  /**
2556
2748
  * Asynchronously retrieves the type of the channel.
@@ -2562,6 +2754,11 @@ var VoiceManager = class extends EventEmitter {
2562
2754
  case DiscordChannelType3.GuildVoice:
2563
2755
  case DiscordChannelType3.GuildStageVoice:
2564
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}`);
2565
2762
  }
2566
2763
  }
2567
2764
  /**
@@ -2592,7 +2789,7 @@ var VoiceManager = class extends EventEmitter {
2592
2789
  const newChannelId = newState.channelId;
2593
2790
  const member = newState.member;
2594
2791
  if (!member) return;
2595
- if (member.id === this.client.user?.id) {
2792
+ if (member.id === this.client?.user?.id) {
2596
2793
  return;
2597
2794
  }
2598
2795
  if (oldChannelId === newChannelId) {
@@ -2626,7 +2823,7 @@ var VoiceManager = class extends EventEmitter {
2626
2823
  adapterCreator: channel.guild.voiceAdapterCreator,
2627
2824
  selfDeaf: false,
2628
2825
  selfMute: false,
2629
- group: this.client.user.id
2826
+ group: this.client?.user?.id ?? "default-group"
2630
2827
  });
2631
2828
  try {
2632
2829
  await Promise.race([
@@ -2702,7 +2899,12 @@ var VoiceManager = class extends EventEmitter {
2702
2899
  * @returns {VoiceConnection | undefined} The voice connection for the specified guild ID, or undefined if not found.
2703
2900
  */
2704
2901
  getVoiceConnection(guildId) {
2705
- 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);
2706
2908
  if (!connections) {
2707
2909
  return;
2708
2910
  }
@@ -2727,6 +2929,7 @@ var VoiceManager = class extends EventEmitter {
2727
2929
  emitClose: true
2728
2930
  });
2729
2931
  if (!receiveStream || receiveStream.readableLength === 0) {
2932
+ logger5.warn(`[monitorMember] No receiveStream or empty stream for user ${entityId}`);
2730
2933
  return;
2731
2934
  }
2732
2935
  const opusDecoder = new prism.opus.Decoder({
@@ -2755,7 +2958,11 @@ var VoiceManager = class extends EventEmitter {
2755
2958
  });
2756
2959
  pipeline(receiveStream, opusDecoder, (err) => {
2757
2960
  if (err) {
2758
- 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
+ );
2759
2966
  }
2760
2967
  });
2761
2968
  this.streams.set(entityId, opusDecoder);
@@ -2780,7 +2987,7 @@ var VoiceManager = class extends EventEmitter {
2780
2987
  opusDecoder.on("error", errorHandler);
2781
2988
  opusDecoder.on("close", closeHandler);
2782
2989
  receiveStream?.on("close", streamCloseHandler);
2783
- this.client.emit("userStream", entityId, name, userName, channel, opusDecoder);
2990
+ this.client?.emit("userStream", entityId, name, userName, channel, opusDecoder);
2784
2991
  }
2785
2992
  /**
2786
2993
  * Leaves the specified voice channel and stops monitoring all members in that channel.
@@ -2795,7 +3002,7 @@ var VoiceManager = class extends EventEmitter {
2795
3002
  this.connections.delete(channel.id);
2796
3003
  }
2797
3004
  for (const [memberId, monitorInfo] of this.activeMonitors) {
2798
- if (monitorInfo.channel.id === channel.id && memberId !== this.client.user?.id) {
3005
+ if (monitorInfo.channel.id === channel.id && memberId !== this.client?.user?.id) {
2799
3006
  this.stopMonitoringMember(memberId);
2800
3007
  }
2801
3008
  }
@@ -2830,8 +3037,10 @@ var VoiceManager = class extends EventEmitter {
2830
3037
  }
2831
3038
  if (this.activeAudioPlayer || this.processingVoice) {
2832
3039
  const state = this.userStates.get(entityId);
2833
- state.buffers.length = 0;
2834
- state.totalLength = 0;
3040
+ if (state) {
3041
+ state.buffers.length = 0;
3042
+ state.totalLength = 0;
3043
+ }
2835
3044
  return;
2836
3045
  }
2837
3046
  if (this.transcriptionTimeout) {
@@ -2911,17 +3120,18 @@ var VoiceManager = class extends EventEmitter {
2911
3120
  const state = this.userStates.get(entityId);
2912
3121
  if (!state || state.buffers.length === 0) return;
2913
3122
  try {
2914
- let isValidTranscription = function(text) {
3123
+ let isValidTranscription2 = function(text) {
2915
3124
  if (!text || text.includes("[BLANK_AUDIO]")) return false;
2916
3125
  return true;
2917
3126
  };
3127
+ var isValidTranscription = isValidTranscription2;
2918
3128
  const inputBuffer = Buffer.concat(state.buffers, state.totalLength);
2919
3129
  state.buffers.length = 0;
2920
3130
  state.totalLength = 0;
2921
3131
  const wavBuffer = await this.convertOpusToWav(inputBuffer);
2922
3132
  logger5.debug("Starting transcription...");
2923
3133
  const transcriptionText = await this.runtime.useModel(ModelType8.TRANSCRIPTION, wavBuffer);
2924
- if (transcriptionText && isValidTranscription(transcriptionText)) {
3134
+ if (transcriptionText && isValidTranscription2(transcriptionText)) {
2925
3135
  state.transcriptionText += transcriptionText;
2926
3136
  }
2927
3137
  if (state.transcriptionText.length) {
@@ -2961,7 +3171,9 @@ var VoiceManager = class extends EventEmitter {
2961
3171
  source: "discord",
2962
3172
  channelId,
2963
3173
  serverId: channel.guild.id,
2964
- type
3174
+ type,
3175
+ worldId: createUniqueUuid5(this.runtime, channel.guild.id),
3176
+ worldName: channel.guild.name
2965
3177
  });
2966
3178
  const memory = {
2967
3179
  id: createUniqueUuid5(this.runtime, `${channelId}-voice-message-${Date.now()}`),
@@ -3185,6 +3397,13 @@ var DiscordService = class _DiscordService extends Service {
3185
3397
  character;
3186
3398
  messageManager;
3187
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;
3188
3407
  /**
3189
3408
  * Constructor for Discord client.
3190
3409
  * Initializes the Discord client with specified intents and partials,
@@ -3194,6 +3413,11 @@ var DiscordService = class _DiscordService extends Service {
3194
3413
  */
3195
3414
  constructor(runtime) {
3196
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
+ }
3197
3421
  const token = runtime.getSetting("DISCORD_API_TOKEN");
3198
3422
  if (!token || token.trim() === "") {
3199
3423
  logger6.warn("Discord API Token not provided - Discord functionality will be unavailable");
@@ -3219,19 +3443,135 @@ var DiscordService = class _DiscordService extends Service {
3219
3443
  this.runtime = runtime;
3220
3444
  this.voiceManager = new VoiceManager(this, runtime);
3221
3445
  this.messageManager = new MessageManager(this);
3222
- this.client.once(Events.ClientReady, this.onClientReady.bind(this));
3446
+ this.client.once(Events.ClientReady, this.onReady.bind(this));
3223
3447
  this.client.login(token).catch((error) => {
3224
- 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
+ );
3225
3451
  this.client = null;
3226
3452
  });
3227
3453
  this.setupEventListeners();
3454
+ this.registerSendHandler();
3228
3455
  } catch (error) {
3229
- logger6.error(`Error initializing Discord client: ${error.message}`);
3456
+ logger6.error(
3457
+ `Error initializing Discord client: ${error instanceof Error ? error.message : String(error)}`
3458
+ );
3230
3459
  this.client = null;
3231
3460
  }
3232
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
+ }
3233
3572
  /**
3234
- * Set up event listeners for the client
3573
+ * Set up event listeners for the client.
3574
+ * @private
3235
3575
  */
3236
3576
  setupEventListeners() {
3237
3577
  if (!this.client) {
@@ -3241,8 +3581,11 @@ var DiscordService = class _DiscordService extends Service {
3241
3581
  if (message.author.id === this.client?.user?.id || message.author.bot) {
3242
3582
  return;
3243
3583
  }
3584
+ if (this.allowedChannelIds && !this.allowedChannelIds.includes(message.channel.id)) {
3585
+ return;
3586
+ }
3244
3587
  try {
3245
- this.messageManager.handleMessage(message);
3588
+ this.messageManager?.handleMessage(message);
3246
3589
  } catch (error) {
3247
3590
  logger6.error(`Error handling message: ${error}`);
3248
3591
  }
@@ -3251,6 +3594,9 @@ var DiscordService = class _DiscordService extends Service {
3251
3594
  if (user.id === this.client?.user?.id) {
3252
3595
  return;
3253
3596
  }
3597
+ if (this.allowedChannelIds && reaction.message.channel && !this.allowedChannelIds.includes(reaction.message.channel.id)) {
3598
+ return;
3599
+ }
3254
3600
  try {
3255
3601
  await this.handleReactionAdd(reaction, user);
3256
3602
  } catch (error) {
@@ -3261,6 +3607,9 @@ var DiscordService = class _DiscordService extends Service {
3261
3607
  if (user.id === this.client?.user?.id) {
3262
3608
  return;
3263
3609
  }
3610
+ if (this.allowedChannelIds && reaction.message.channel && !this.allowedChannelIds.includes(reaction.message.channel.id)) {
3611
+ return;
3612
+ }
3264
3613
  try {
3265
3614
  await this.handleReactionRemove(reaction, user);
3266
3615
  } catch (error) {
@@ -3282,6 +3631,9 @@ var DiscordService = class _DiscordService extends Service {
3282
3631
  }
3283
3632
  });
3284
3633
  this.client.on("interactionCreate", async (interaction) => {
3634
+ if (this.allowedChannelIds && interaction.channelId && !this.allowedChannelIds.includes(interaction.channelId)) {
3635
+ return;
3636
+ }
3285
3637
  try {
3286
3638
  await this.handleInteractionCreate(interaction);
3287
3639
  } catch (error) {
@@ -3290,7 +3642,7 @@ var DiscordService = class _DiscordService extends Service {
3290
3642
  });
3291
3643
  this.client.on("userStream", (entityId, name, userName, channel, opusDecoder) => {
3292
3644
  if (entityId !== this.client?.user?.id) {
3293
- this.voiceManager.handleUserStream(entityId, name, userName, channel, opusDecoder);
3645
+ this.voiceManager?.handleUserStream(entityId, name, userName, channel, opusDecoder);
3294
3646
  }
3295
3647
  });
3296
3648
  }
@@ -3299,6 +3651,7 @@ var DiscordService = class _DiscordService extends Service {
3299
3651
  *
3300
3652
  * @param {GuildMember} member - The GuildMember object representing the new member that joined the guild.
3301
3653
  * @returns {Promise<void>} - A Promise that resolves once the event handling is complete.
3654
+ * @private
3302
3655
  */
3303
3656
  async handleGuildMemberAdd(member) {
3304
3657
  logger6.log(`New member joined: ${member.user.username}`);
@@ -3328,164 +3681,442 @@ var DiscordService = class _DiscordService extends Service {
3328
3681
  });
3329
3682
  }
3330
3683
  /**
3331
- *
3332
- * Start the Discord service
3333
- * @param {IAgentRuntime} runtime - The runtime for the agent
3334
- * @returns {Promise<DiscordService>} A promise that resolves to a DiscordService instance
3335
- *
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
3336
3688
  */
3337
- static async start(runtime) {
3338
- const token = runtime.getSetting("DISCORD_API_TOKEN");
3339
- if (!token || token.trim() === "") {
3340
- 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
+ }
3341
3736
  }
3342
- const maxRetries = 5;
3343
- let retryCount = 0;
3344
- let lastError = null;
3345
- 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
+ }
3346
3749
  try {
3347
- const service = new _DiscordService(runtime);
3348
- if (!service.client) {
3349
- 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();
3350
3763
  }
3351
- await new Promise((resolve, reject) => {
3352
- const timeout = setTimeout(() => {
3353
- reject(new Error("Discord client ready timeout"));
3354
- }, 3e4);
3355
- service.client?.once("ready", () => {
3356
- clearTimeout(timeout);
3357
- 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"
3358
3779
  });
3359
- });
3360
- 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
+ }
3361
3788
  } catch (error) {
3362
- lastError = error instanceof Error ? error : new Error(String(error));
3363
- logger6.error(
3364
- `Discord initialization attempt ${retryCount + 1} failed: ${lastError.message}`
3365
- );
3366
- retryCount++;
3367
- if (retryCount < maxRetries) {
3368
- const delay = 2 ** retryCount * 1e3;
3369
- logger6.info(`Retrying Discord initialization in ${delay / 1e3} seconds...`);
3370
- 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}`);
3371
3797
  }
3372
3798
  }
3373
3799
  }
3374
- throw new Error(
3375
- `Discord initialization failed after ${maxRetries} attempts. Last error: ${lastError?.message}`
3376
- );
3377
3800
  }
3378
3801
  /**
3379
- * Stops the Discord client associated with the given runtime.
3802
+ * Builds a standardized list of rooms from Discord guild channels.
3380
3803
  *
3381
- * @param {IAgentRuntime} runtime - The runtime associated with the Discord client.
3382
- * @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
3383
3808
  */
3384
- static async stop(runtime) {
3385
- const client = runtime.getService(DISCORD_SERVICE_NAME);
3386
- if (!client) {
3387
- logger6.error("DiscordService not found");
3388
- return;
3389
- }
3390
- try {
3391
- await client.stop();
3392
- } catch (e) {
3393
- 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
+ }
3394
3843
  }
3844
+ return rooms;
3395
3845
  }
3396
3846
  /**
3397
- * 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.
3398
3849
  *
3399
- * @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
3400
3853
  */
3401
- async stop() {
3402
- await this.client?.destroy();
3403
- }
3404
- /**
3405
- * Handle the event when the client is ready.
3406
- * @param {Object} readyClient - The ready client object containing user information.
3407
- * @param {string} readyClient.user.tag - The username and discriminator of the client user.
3408
- * @param {string} readyClient.user.id - The user ID of the client.
3409
- * @returns {Promise<void>}
3410
- */
3411
- async onClientReady(readyClient) {
3412
- logger6.success(`DISCORD: Logged in as ${readyClient.user?.tag}`);
3413
- const commands = [
3414
- {
3415
- name: "joinchannel",
3416
- description: "Join a voice channel",
3417
- options: [
3418
- {
3419
- name: "channel",
3420
- type: 7,
3421
- // CHANNEL type
3422
- description: "The voice channel to join",
3423
- required: true,
3424
- channel_types: [2]
3425
- // GuildVoice type
3426
- }
3427
- ]
3428
- },
3429
- {
3430
- name: "leavechannel",
3431
- description: "Leave the current voice channel"
3432
- }
3433
- ];
3434
- try {
3435
- await this.client?.application?.commands.set(commands);
3436
- logger6.success("DISCORD: Slash commands registered");
3437
- } catch (error) {
3438
- console.error("Error registering slash commands:", error);
3439
- }
3440
- const requiredPermissions = [
3441
- // Text Permissions
3442
- PermissionsBitField2.Flags.ViewChannel,
3443
- PermissionsBitField2.Flags.SendMessages,
3444
- PermissionsBitField2.Flags.SendMessagesInThreads,
3445
- PermissionsBitField2.Flags.CreatePrivateThreads,
3446
- PermissionsBitField2.Flags.CreatePublicThreads,
3447
- PermissionsBitField2.Flags.EmbedLinks,
3448
- PermissionsBitField2.Flags.AttachFiles,
3449
- PermissionsBitField2.Flags.AddReactions,
3450
- PermissionsBitField2.Flags.UseExternalEmojis,
3451
- PermissionsBitField2.Flags.UseExternalStickers,
3452
- PermissionsBitField2.Flags.MentionEveryone,
3453
- PermissionsBitField2.Flags.ManageMessages,
3454
- PermissionsBitField2.Flags.ReadMessageHistory,
3455
- // Voice Permissions
3456
- PermissionsBitField2.Flags.Connect,
3457
- PermissionsBitField2.Flags.Speak,
3458
- PermissionsBitField2.Flags.UseVAD,
3459
- PermissionsBitField2.Flags.PrioritySpeaker
3460
- ].reduce((a, b) => a | b, 0n);
3461
- logger6.success("Use this URL to add the bot to your server:");
3462
- logger6.success(
3463
- `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user?.id}&permissions=${requiredPermissions}&scope=bot%20applications.commands`
3464
- );
3465
- await this.onReady();
3466
- }
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
+ });
3892
+ }
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);
3935
+ }
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;
3979
+ }
3467
3980
  /**
3468
- * Asynchronously retrieves the type of a given channel.
3469
- *
3470
- * @param {Channel} channel - The channel for which to determine the type.
3471
- * @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.
3472
3985
  */
3473
- async getChannelType(channel) {
3474
- switch (channel.type) {
3475
- case DiscordChannelType4.DM:
3476
- return ChannelType9.DM;
3477
- case DiscordChannelType4.GuildText:
3478
- return ChannelType9.GROUP;
3479
- case DiscordChannelType4.GuildVoice:
3480
- 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);
3481
4032
  }
4033
+ this.client?.emit("voiceManagerReady");
3482
4034
  }
3483
4035
  /**
3484
- * 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.
3485
4053
  *
3486
- * @param {MessageReaction | PartialMessageReaction} reaction The reaction that was added.
3487
- * @param {User | PartialUser} user The user who added the reaction.
3488
- * @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
3489
4120
  */
3490
4121
  async handleReactionAdd(reaction, user) {
3491
4122
  try {
@@ -3522,13 +4153,15 @@ var DiscordService = class _DiscordService extends Service {
3522
4153
  }
3523
4154
  const messageContent = reaction.message.content || "";
3524
4155
  const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3525
- const reactionMessage = `*Added <${emoji}> to: "${truncatedContent}"*`;
4156
+ const reactionMessage = `*Added <${emoji}> to: \\"${truncatedContent}\\"*`;
3526
4157
  const userName = reaction.message.author?.username || "unknown";
3527
4158
  const name = reaction.message.author?.displayName || userName;
3528
4159
  await this.runtime.ensureConnection({
3529
4160
  entityId,
3530
4161
  roomId,
3531
4162
  userName,
4163
+ worldId: createUniqueUuid6(this.runtime, reaction.message.guild?.id ?? roomId),
4164
+ worldName: reaction.message.guild?.name,
3532
4165
  name,
3533
4166
  source: "discord",
3534
4167
  channelId: reaction.message.channel.id,
@@ -3554,9 +4187,9 @@ var DiscordService = class _DiscordService extends Service {
3554
4187
  const callback = async (content) => {
3555
4188
  if (!reaction.message.channel) {
3556
4189
  logger6.error("No channel found for reaction message");
3557
- return;
4190
+ return [];
3558
4191
  }
3559
- await reaction.message.channel.send(content.text);
4192
+ await reaction.message.channel.send(content.text ?? "");
3560
4193
  return [];
3561
4194
  };
3562
4195
  this.runtime.emitEvent(["DISCORD_REACTION_RECEIVED", "REACTION_RECEIVED"], {
@@ -3569,11 +4202,8 @@ var DiscordService = class _DiscordService extends Service {
3569
4202
  }
3570
4203
  }
3571
4204
  /**
3572
- * Handles the removal of a reaction on a message.
3573
- *
3574
- * @param {MessageReaction | PartialMessageReaction} reaction - The reaction that was removed.
3575
- * @param {User | PartialUser} user - The user who removed the reaction.
3576
- * @returns {Promise<void>} - A Promise that resolves after handling the reaction removal.
4205
+ * Placeholder for handling reaction removal.
4206
+ * @private
3577
4207
  */
3578
4208
  async handleReactionRemove(reaction, user) {
3579
4209
  try {
@@ -3592,7 +4222,7 @@ var DiscordService = class _DiscordService extends Service {
3592
4222
  }
3593
4223
  const messageContent = reaction.message.content || "";
3594
4224
  const truncatedContent = messageContent.length > 50 ? `${messageContent.substring(0, 50)}...` : messageContent;
3595
- const reactionMessage = `*Removed <${emoji}> from: "${truncatedContent}"*`;
4225
+ const reactionMessage = `*Removed <${emoji}> from: \\"${truncatedContent}\\"*`;
3596
4226
  const roomId = createUniqueUuid6(this.runtime, reaction.message.channel.id);
3597
4227
  const entityId = createUniqueUuid6(this.runtime, user.id);
3598
4228
  const timestamp = Date.now();
@@ -3606,6 +4236,8 @@ var DiscordService = class _DiscordService extends Service {
3606
4236
  entityId,
3607
4237
  roomId,
3608
4238
  userName,
4239
+ worldId: createUniqueUuid6(this.runtime, reaction.message.guild?.id ?? roomId),
4240
+ worldName: reaction.message.guild?.name,
3609
4241
  name,
3610
4242
  source: "discord",
3611
4243
  channelId: reaction.message.channel.id,
@@ -3630,9 +4262,9 @@ var DiscordService = class _DiscordService extends Service {
3630
4262
  const callback = async (content) => {
3631
4263
  if (!reaction.message.channel) {
3632
4264
  logger6.error("No channel found for reaction message");
3633
- return;
4265
+ return [];
3634
4266
  }
3635
- await reaction.message.channel.send(content.text);
4267
+ await reaction.message.channel.send(content.text ?? "");
3636
4268
  return [];
3637
4269
  };
3638
4270
  this.runtime.emitEvent(["DISCORD_REACTION_RECEIVED" /* REACTION_RECEIVED */], {
@@ -3645,260 +4277,40 @@ var DiscordService = class _DiscordService extends Service {
3645
4277
  }
3646
4278
  }
3647
4279
  /**
3648
- * 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.
3649
- * @param {Guild} guild - The guild that the bot has joined.
3650
- * @returns {Promise<void>}
4280
+ * Stops the Discord service and cleans up resources.
4281
+ * Implements the abstract method from the Service class.
3651
4282
  */
3652
- async handleGuildCreate(guild) {
3653
- logger6.log(`Joined guild ${guild.name}`);
3654
- const fullGuild = await guild.fetch();
3655
- this.voiceManager.scanGuild(guild);
3656
- const ownerId = createUniqueUuid6(this.runtime, fullGuild.ownerId);
3657
- const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
3658
- const standardizedData = {
3659
- runtime: this.runtime,
3660
- rooms: await this.buildStandardizedRooms(fullGuild, worldId),
3661
- users: await this.buildStandardizedUsers(fullGuild),
3662
- world: {
3663
- id: worldId,
3664
- name: fullGuild.name,
3665
- agentId: this.runtime.agentId,
3666
- serverId: fullGuild.id,
3667
- metadata: {
3668
- ownership: fullGuild.ownerId ? { ownerId } : void 0,
3669
- roles: {
3670
- [ownerId]: Role.OWNER
3671
- }
3672
- }
3673
- },
3674
- source: "discord"
3675
- };
3676
- this.runtime.emitEvent(["DISCORD_WORLD_JOINED" /* WORLD_JOINED */], {
3677
- runtime: this.runtime,
3678
- server: fullGuild,
3679
- source: "discord"
3680
- });
3681
- this.runtime.emitEvent([EventType2.WORLD_JOINED], standardizedData);
3682
- }
3683
- /**
3684
- * Handles interactions created by the user, specifically commands.
3685
- * @param {any} interaction - The interaction object received
3686
- * @returns {void}
3687
- */
3688
- async handleInteractionCreate(interaction) {
3689
- if (!interaction.isCommand()) return;
3690
- switch (interaction.commandName) {
3691
- case "joinchannel":
3692
- await this.voiceManager.handleJoinChannelCommand(interaction);
3693
- break;
3694
- case "leavechannel":
3695
- await this.voiceManager.handleLeaveChannelCommand(interaction);
3696
- 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.");
3697
4291
  }
3698
- }
3699
- /**
3700
- * Builds a standardized list of rooms from Discord guild channels
3701
- */
3702
- /**
3703
- * Build standardized rooms for a guild based on text and voice channels.
3704
- *
3705
- * @param {Guild} guild The guild to build rooms for.
3706
- * @param {UUID} _worldId The ID of the world to associate with the rooms.
3707
- * @returns {Promise<any[]>} An array of standardized room objects.
3708
- */
3709
- async buildStandardizedRooms(guild, _worldId) {
3710
- const rooms = [];
3711
- for (const [channelId, channel] of guild.channels.cache) {
3712
- if (channel.type === DiscordChannelType4.GuildText || channel.type === DiscordChannelType4.GuildVoice) {
3713
- const roomId = createUniqueUuid6(this.runtime, channelId);
3714
- let channelType;
3715
- switch (channel.type) {
3716
- case DiscordChannelType4.GuildText:
3717
- channelType = ChannelType9.GROUP;
3718
- break;
3719
- case DiscordChannelType4.GuildVoice:
3720
- channelType = ChannelType9.VOICE_GROUP;
3721
- break;
3722
- default:
3723
- channelType = ChannelType9.GROUP;
3724
- }
3725
- let participants = [];
3726
- if (guild.memberCount < 1e3 && channel.type === DiscordChannelType4.GuildText) {
3727
- try {
3728
- participants = Array.from(guild.members.cache.values()).filter(
3729
- (member) => channel.permissionsFor(member)?.has(PermissionsBitField2.Flags.ViewChannel)
3730
- ).map((member) => createUniqueUuid6(this.runtime, member.id));
3731
- } catch (error) {
3732
- logger6.warn(`Failed to get participants for channel ${channel.name}:`, error);
3733
- }
3734
- }
3735
- rooms.push({
3736
- id: roomId,
3737
- name: channel.name,
3738
- type: channelType,
3739
- channelId: channel.id,
3740
- participants
3741
- });
3742
- }
4292
+ if (this.voiceManager) {
3743
4293
  }
3744
- return rooms;
4294
+ logger6.info("Discord service stopped.");
3745
4295
  }
3746
4296
  /**
3747
- * 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.
3748
4301
  */
3749
- async buildStandardizedUsers(guild) {
3750
- const entities = [];
3751
- const botId = this.client?.user?.id;
3752
- if (guild.memberCount > 1e3) {
3753
- logger6.info(
3754
- `Using optimized user sync for large guild ${guild.name} (${guild.memberCount} members)`
3755
- );
3756
- try {
3757
- for (const [, member] of guild.members.cache) {
3758
- const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3759
- if (member.id !== botId) {
3760
- entities.push({
3761
- id: createUniqueUuid6(this.runtime, member.id),
3762
- names: Array.from(
3763
- /* @__PURE__ */ new Set([member.user.username, member.displayName, member.user.globalName])
3764
- ),
3765
- agentId: this.runtime.agentId,
3766
- metadata: {
3767
- default: {
3768
- username: tag,
3769
- name: member.displayName || member.user.username
3770
- },
3771
- discord: member.user.globalName ? {
3772
- username: tag,
3773
- name: member.displayName || member.user.username,
3774
- globalName: member.user.globalName,
3775
- userId: member.id
3776
- } : {
3777
- username: tag,
3778
- name: member.displayName || member.user.username,
3779
- userId: member.id
3780
- }
3781
- }
3782
- });
3783
- }
3784
- }
3785
- if (entities.length < 100) {
3786
- logger6.info(`Adding online members for ${guild.name}`);
3787
- const onlineMembers = await guild.members.fetch({ limit: 100 });
3788
- for (const [, member] of onlineMembers) {
3789
- if (member.id !== botId) {
3790
- const entityId = createUniqueUuid6(this.runtime, member.id);
3791
- if (!entities.some((u) => u.id === entityId)) {
3792
- const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3793
- entities.push({
3794
- id: entityId,
3795
- names: Array.from(
3796
- /* @__PURE__ */ new Set([member.user.username, member.displayName, member.user.globalName])
3797
- ),
3798
- agentId: this.runtime.agentId,
3799
- metadata: {
3800
- default: {
3801
- username: tag,
3802
- name: member.displayName || member.user.username
3803
- },
3804
- discord: member.user.globalName ? {
3805
- username: tag,
3806
- name: member.displayName || member.user.username,
3807
- globalName: member.user.globalName,
3808
- userId: member.id
3809
- } : {
3810
- username: tag,
3811
- name: member.displayName || member.user.username,
3812
- userId: member.id
3813
- }
3814
- }
3815
- });
3816
- }
3817
- }
3818
- }
3819
- }
3820
- } catch (error) {
3821
- logger6.error(`Error fetching members for ${guild.name}:`, error);
3822
- }
3823
- } else {
3824
- try {
3825
- let members = guild.members.cache;
3826
- if (members.size === 0) {
3827
- members = await guild.members.fetch();
3828
- }
3829
- for (const [, member] of members) {
3830
- if (member.id !== botId) {
3831
- const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
3832
- entities.push({
3833
- id: createUniqueUuid6(this.runtime, member.id),
3834
- names: Array.from(
3835
- /* @__PURE__ */ new Set([member.user.username, member.displayName, member.user.globalName])
3836
- ),
3837
- agentId: this.runtime.agentId,
3838
- metadata: {
3839
- default: {
3840
- username: tag,
3841
- name: member.displayName || member.user.username
3842
- },
3843
- discord: member.user.globalName ? {
3844
- username: tag,
3845
- name: member.displayName || member.user.username,
3846
- globalName: member.user.globalName,
3847
- userId: member.id
3848
- } : {
3849
- username: tag,
3850
- name: member.displayName || member.user.username,
3851
- userId: member.id
3852
- }
3853
- }
3854
- });
3855
- }
3856
- }
3857
- } catch (error) {
3858
- logger6.error(`Error fetching members for ${guild.name}:`, error);
3859
- }
3860
- }
3861
- return entities;
3862
- }
3863
- async onReady() {
3864
- logger6.log("DISCORD ON READY");
3865
- const guilds = await this.client?.guilds.fetch();
3866
- for (const [, guild] of guilds) {
3867
- const fullGuild = await guild.fetch();
3868
- await this.voiceManager.scanGuild(fullGuild);
3869
- setTimeout(async () => {
3870
- const fullGuild2 = await guild.fetch();
3871
- logger6.log("DISCORD SERVER CONNECTED", fullGuild2.name);
3872
- this.runtime.emitEvent(["DISCORD_SERVER_CONNECTED" /* WORLD_CONNECTED */], {
3873
- runtime: this.runtime,
3874
- server: fullGuild2,
3875
- source: "discord"
3876
- });
3877
- const worldId = createUniqueUuid6(this.runtime, fullGuild2.id);
3878
- const ownerId = createUniqueUuid6(this.runtime, fullGuild2.ownerId);
3879
- const standardizedData = {
3880
- name: fullGuild2.name,
3881
- runtime: this.runtime,
3882
- rooms: await this.buildStandardizedRooms(fullGuild2, worldId),
3883
- entities: await this.buildStandardizedUsers(fullGuild2),
3884
- world: {
3885
- id: worldId,
3886
- name: fullGuild2.name,
3887
- agentId: this.runtime.agentId,
3888
- serverId: fullGuild2.id,
3889
- metadata: {
3890
- ownership: fullGuild2.ownerId ? { ownerId } : void 0,
3891
- roles: {
3892
- [ownerId]: Role.OWNER
3893
- }
3894
- }
3895
- },
3896
- source: "discord"
3897
- };
3898
- this.runtime.emitEvent([EventType2.WORLD_CONNECTED], standardizedData);
3899
- }, 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;
3900
4313
  }
3901
- this.client?.emit("voiceManagerReady");
3902
4314
  }
3903
4315
  };
3904
4316
 
@@ -3912,11 +4324,12 @@ import {
3912
4324
  entersState as entersState2
3913
4325
  } from "@discordjs/voice";
3914
4326
  import { ModelType as ModelType9, logger as logger7 } from "@elizaos/core";
3915
- import { ChannelType as ChannelType10, Events as Events2 } from "discord.js";
4327
+ import { ChannelType as ChannelType10, Events as Events2, AttachmentBuilder } from "discord.js";
3916
4328
  var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
3917
4329
  var DiscordTestSuite = class {
3918
4330
  name = "discord";
3919
- discordClient = null;
4331
+ discordClient;
4332
+ // Use definite assignment assertion
3920
4333
  tests;
3921
4334
  /**
3922
4335
  * Constructor for initializing the tests array with test cases to be executed.
@@ -3962,13 +4375,19 @@ var DiscordTestSuite = class {
3962
4375
  async testCreatingDiscordClient(runtime) {
3963
4376
  try {
3964
4377
  this.discordClient = runtime.getService(ServiceType2.DISCORD);
3965
- 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()) {
3966
4382
  logger7.success("DiscordService is already ready.");
3967
4383
  } else {
3968
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
+ }
3969
4388
  await new Promise((resolve, reject) => {
3970
- this.discordClient.client.once(Events2.ClientReady, resolve);
3971
- this.discordClient.client.once(Events2.Error, reject);
4389
+ this.discordClient.client?.once(Events2.ClientReady, resolve);
4390
+ this.discordClient.client?.once(Events2.Error, reject);
3972
4391
  });
3973
4392
  }
3974
4393
  } catch (error) {
@@ -3983,6 +4402,7 @@ var DiscordTestSuite = class {
3983
4402
  * @throws {Error} - If there is an error in executing the slash command test.
3984
4403
  */
3985
4404
  async testJoinVoiceSlashCommand(runtime) {
4405
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
3986
4406
  try {
3987
4407
  await this.waitForVoiceManagerReady(this.discordClient);
3988
4408
  const channel = await this.getTestChannel(runtime);
@@ -4002,10 +4422,13 @@ var DiscordTestSuite = class {
4002
4422
  logger7.info(`JoinChannel Slash Command Response: ${message}`);
4003
4423
  }
4004
4424
  };
4425
+ if (!this.discordClient.voiceManager) {
4426
+ throw new Error("VoiceManager is not available on the Discord client.");
4427
+ }
4005
4428
  await this.discordClient.voiceManager.handleJoinChannelCommand(fakeJoinInteraction);
4006
- logger7.success("Slash command test completed successfully.");
4429
+ logger7.success("Join voice slash command test completed successfully.");
4007
4430
  } catch (error) {
4008
- throw new Error(`Error in slash commands test: ${error}`);
4431
+ throw new Error(`Error in join voice slash commands test: ${error}`);
4009
4432
  }
4010
4433
  }
4011
4434
  /**
@@ -4015,6 +4438,7 @@ var DiscordTestSuite = class {
4015
4438
  * @returns {Promise<void>} A promise that resolves when the test is complete.
4016
4439
  */
4017
4440
  async testLeaveVoiceSlashCommand(runtime) {
4441
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4018
4442
  try {
4019
4443
  await this.waitForVoiceManagerReady(this.discordClient);
4020
4444
  const channel = await this.getTestChannel(runtime);
@@ -4029,10 +4453,13 @@ var DiscordTestSuite = class {
4029
4453
  logger7.info(`LeaveChannel Slash Command Response: ${message}`);
4030
4454
  }
4031
4455
  };
4456
+ if (!this.discordClient.voiceManager) {
4457
+ throw new Error("VoiceManager is not available on the Discord client.");
4458
+ }
4032
4459
  await this.discordClient.voiceManager.handleLeaveChannelCommand(fakeLeaveInteraction);
4033
- logger7.success("Slash command test completed successfully.");
4460
+ logger7.success("Leave voice slash command test completed successfully.");
4034
4461
  } catch (error) {
4035
- throw new Error(`Error in slash commands test: ${error}`);
4462
+ throw new Error(`Error in leave voice slash commands test: ${error}`);
4036
4463
  }
4037
4464
  }
4038
4465
  /**
@@ -4041,16 +4468,26 @@ var DiscordTestSuite = class {
4041
4468
  * @throws {Error} - If voice channel is invalid, voice connection fails to become ready, or no text to speech service found.
4042
4469
  */
4043
4470
  async testTextToSpeechPlayback(runtime) {
4471
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4044
4472
  try {
4045
4473
  await this.waitForVoiceManagerReady(this.discordClient);
4046
4474
  const channel = await this.getTestChannel(runtime);
4047
4475
  if (!channel || channel.type !== ChannelType10.GuildVoice) {
4048
4476
  throw new Error("Invalid voice channel.");
4049
4477
  }
4478
+ if (!this.discordClient.voiceManager) {
4479
+ throw new Error("VoiceManager is not available on the Discord client.");
4480
+ }
4050
4481
  await this.discordClient.voiceManager.joinChannel(channel);
4051
4482
  const guild = await this.getActiveGuild(this.discordClient);
4052
4483
  const guildId = guild.id;
4484
+ if (!this.discordClient.voiceManager) {
4485
+ throw new Error("VoiceManager is not available on the Discord client.");
4486
+ }
4053
4487
  const connection = this.discordClient.voiceManager.getVoiceConnection(guildId);
4488
+ if (!connection) {
4489
+ throw new Error(`No voice connection found for guild: ${guildId}`);
4490
+ }
4054
4491
  try {
4055
4492
  await entersState2(connection, VoiceConnectionStatus2.Ready, 1e4);
4056
4493
  logger7.success(`Voice connection is ready in guild: ${guildId}`);
@@ -4082,9 +4519,14 @@ var DiscordTestSuite = class {
4082
4519
  * @throws {Error} If there is an error in sending the text message.
4083
4520
  */
4084
4521
  async testSendingTextMessage(runtime) {
4522
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4085
4523
  try {
4086
4524
  const channel = await this.getTestChannel(runtime);
4087
- 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]);
4088
4530
  } catch (error) {
4089
4531
  throw new Error(`Error in sending text message: ${error}`);
4090
4532
  }
@@ -4096,6 +4538,7 @@ var DiscordTestSuite = class {
4096
4538
  * @returns {Promise<void>} A Promise that resolves once the message is handled.
4097
4539
  */
4098
4540
  async testHandlingMessage(runtime) {
4541
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4099
4542
  try {
4100
4543
  const channel = await this.getTestChannel(runtime);
4101
4544
  const fakeMessage = {
@@ -4114,9 +4557,12 @@ var DiscordTestSuite = class {
4114
4557
  reference: null,
4115
4558
  attachments: []
4116
4559
  };
4560
+ if (!this.discordClient.messageManager) {
4561
+ throw new Error("MessageManager is not available on the Discord client.");
4562
+ }
4117
4563
  await this.discordClient.messageManager.handleMessage(fakeMessage);
4118
4564
  } catch (error) {
4119
- throw new Error(`Error in sending text message: ${error}`);
4565
+ throw new Error(`Error in handling message test: ${error}`);
4120
4566
  }
4121
4567
  }
4122
4568
  // #############################
@@ -4130,8 +4576,9 @@ var DiscordTestSuite = class {
4130
4576
  * @throws {Error} If no test channel is found.
4131
4577
  */
4132
4578
  async getTestChannel(runtime) {
4579
+ if (!this.discordClient) throw new Error("Discord client not initialized.");
4133
4580
  const channelId = this.validateChannelId(runtime);
4134
- const channel = await this.discordClient.client.channels.fetch(channelId);
4581
+ const channel = await this.discordClient.client?.channels.fetch(channelId);
4135
4582
  if (!channel) throw new Error("no test channel found!");
4136
4583
  return channel;
4137
4584
  }
@@ -4149,7 +4596,7 @@ var DiscordTestSuite = class {
4149
4596
  if (!channel || !channel.isTextBased()) {
4150
4597
  throw new Error("Channel is not a text-based channel or does not exist.");
4151
4598
  }
4152
- await sendMessageInChunks(channel, messageContent, null, files);
4599
+ await sendMessageInChunks(channel, messageContent, "", files);
4153
4600
  } catch (error) {
4154
4601
  throw new Error(`Error sending message: ${error}`);
4155
4602
  }
@@ -4190,6 +4637,9 @@ var DiscordTestSuite = class {
4190
4637
  * @throws {Error} If no active voice connection is found for the bot.
4191
4638
  */
4192
4639
  async getActiveGuild(discordClient) {
4640
+ if (!discordClient.client) {
4641
+ throw new Error("Discord client instance is missing within the service.");
4642
+ }
4193
4643
  const guilds = await discordClient.client.guilds.fetch();
4194
4644
  const fullGuilds = await Promise.all(guilds.map((guild) => guild.fetch()));
4195
4645
  const activeGuild = fullGuilds.find((g) => g.members.me?.voice.channelId);
@@ -4209,10 +4659,13 @@ var DiscordTestSuite = class {
4209
4659
  if (!discordClient) {
4210
4660
  throw new Error("Discord client is not initialized.");
4211
4661
  }
4212
- 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()) {
4213
4666
  await new Promise((resolve, reject) => {
4214
- discordClient.voiceManager.once("ready", resolve);
4215
- discordClient.voiceManager.once("error", reject);
4667
+ discordClient.voiceManager?.once("ready", resolve);
4668
+ discordClient.voiceManager?.once("error", reject);
4216
4669
  });
4217
4670
  }
4218
4671
  }