Skip to content

Commit 78ecd0f

Browse files
authored
feat(discord): add tiered permission system for bot invite URLs (#33)
* feat(discord): add tiered permission system for bot invite URLs Add a 3x2 permission matrix for Discord bot invites: - Role levels: Basic / Moderator / Admin - Voice options: With Voice / Without Voice Changes: - Add src/permissions.ts with centralized permission tier definitions - Enhance banner.ts with tiered invite URL display (🎙️/💬 sections) - Update index.ts to use new banner with all settings displayed - Refactor service.ts to use generateInviteUrl() instead of inline perms - Add extraMetadata to buildMemoryFromMessage for cross-agent filtering - Fix channelState/voiceState providers to handle both channelId and serverId - Export permission utilities (DiscordPermissionTiers, generateInviteUrl, etc) Permission tiers: - Basic: text chat, reactions, embeds, threads, slash commands - Moderator: + manage messages, mention everyone, polls, timeout - Admin: + kick/ban, manage channels/roles/webhooks - Voice addon: connect, speak, VAD, priority speaker, stream * fix(discord): address null safety and linting issues - banner.ts: Remove redundant 'as string' type assertion - readChannel.ts: Add explicit null check for serverId before guild fetch - searchMessages.ts: Add explicit null check for serverId before guild fetch - voiceState.ts: Remove unused serverId variable (guildId from channel is correct) - service.ts: Guard invite URL generation - only build when user ID is available, log warning when unavailable instead of invalid URL with 'undefined' * fix(discord): remove unused imports and variables - service.ts: Remove unused DiscordPermissionTiers import - channelState.ts: Remove unused serverId variable * feat(discord): add raw Discord IDs to message metadata for cross-agent correlation Add discordMessageId, discordChannelId, and discordServerId to message metadata. These raw Discord snowflake IDs (not transformed by createUniqueUuid) allow different agents to recognize they're looking at the same Discord message, enabling cross-agent coordination and deduplication. * fix(discord): fix operator mismatch and mask boundary bugs - readChannel.ts, searchMessages.ts: Change ?? to || for serverId assignment to match condition semantics (empty strings should fallback to messageServerId) - banner.ts: Fix mask function off-by-one - 8-char strings were fully revealed because length-8=0 bullets; now uses <= 8 to fully mask short values * refactor(discord): remove duplicate DiscordPermissionValues interface Import DiscordPermissionValues from ./permissions using type-only import instead of duplicating the interface definition in banner.ts * fix(discord): remove unreachable dead code in readChannel and searchMessages The if (!serverId) checks were unreachable because the enclosing else if (room?.serverId || room?.messageServerId) branch only enters when at least one value is truthy, and the || assignment guarantees serverId is truthy. Remove the dead error handling code. * fix(discord): correctly truncate ANSI-colored strings in banner The line() function now truncates based on visible character positions instead of raw string positions. This prevents cutting in the middle of ANSI escape sequences (like \x1b[91m) which would produce malformed terminal output. Also appends ANSI reset after truncation to close any unclosed color sequences. * fix(discord): make printDiscordBanner synchronous The deprecated printDiscordBanner function previously used a dynamic import('./permissions').then() pattern which caused the function to return immediately while the banner printed asynchronously. Since permissions.ts doesn't import from banner.ts, there's no circular dependency - import getPermissionValues directly and call printBanner synchronously so callers can rely on the banner being printed when the function returns.
1 parent 1c5665f commit 78ecd0f

12 files changed

Lines changed: 660 additions & 107 deletions

File tree

src/actions/getUserInfo.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,16 @@ export const getUserInfo: Action = {
149149

150150
try {
151151
const room = state.data?.room || (await runtime.getRoom(message.roomId));
152-
if (!room?.serverId) {
152+
const serverId = room?.serverId ?? room?.messageServerId;
153+
if (!serverId) {
153154
await callback({
154155
text: "I couldn't determine the current server.",
155156
source: 'discord',
156157
});
157158
return;
158159
}
159160

160-
const guild = await discordService.client.guilds.fetch(room.serverId);
161+
const guild = await discordService.client.guilds.fetch(serverId);
161162

162163
let member: GuildMember | null = null;
163164

src/actions/readChannel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,11 @@ export const readChannel: Action = {
161161
targetChannel = (await discordService.client.channels.fetch(
162162
channelInfo.channelIdentifier
163163
)) as TextChannel;
164-
} else if (room?.serverId) {
164+
} else if (room?.serverId || room?.messageServerId) {
165165
// It's a channel name - search in the current server
166-
const guild = await discordService.client.guilds.fetch(room.serverId);
166+
// serverId is guaranteed truthy since we're inside the || condition
167+
const serverId = (room?.serverId || room?.messageServerId)!;
168+
const guild = await discordService.client.guilds.fetch(serverId);
167169
const channels = await guild.channels.fetch();
168170

169171
targetChannel =

src/actions/searchMessages.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,10 @@ export const searchMessages: Action = {
193193
targetChannel = (await discordService.client.channels.fetch(
194194
searchParams.channelIdentifier
195195
)) as TextChannel;
196-
} else if (room?.serverId) {
197-
const guild = await discordService.client.guilds.fetch(room.serverId);
196+
} else if (room?.serverId || room?.messageServerId) {
197+
// serverId is guaranteed truthy since we're inside the || condition
198+
const serverId = (room?.serverId || room?.messageServerId)!;
199+
const guild = await discordService.client.guilds.fetch(serverId);
198200
const channels = await guild.channels.fetch();
199201
targetChannel =
200202
(channels.find(

src/actions/serverInfo.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import { type Guild } from 'discord.js';
1313

1414
const formatServerInfo = (guild: Guild, detailed: boolean = false): string => {
1515
const createdAt = new Date(guild.createdAt).toLocaleDateString();
16-
const memberCount = guild.memberCount;
17-
const channelCount = guild.channels.cache.size;
18-
const roleCount = guild.roles.cache.size;
19-
const emojiCount = guild.emojis.cache.size;
16+
const memberCount = guild.memberCount.toLocaleString();
17+
const channelCount = guild.channels.cache.size.toLocaleString();
18+
const roleCount = guild.roles.cache.size.toLocaleString();
19+
const emojiCount = guild.emojis.cache.size.toLocaleString();
2020
const boostLevel = guild.premiumTier;
21-
const boostCount = guild.premiumSubscriptionCount || 0;
21+
const boostCount = (guild.premiumSubscriptionCount || 0).toLocaleString();
2222

2323
const basicInfo = [
2424
`🏛️ **Server Information for ${guild.name}**`,
@@ -32,10 +32,11 @@ const formatServerInfo = (guild: Guild, detailed: boolean = false): string => {
3232
];
3333

3434
if (detailed) {
35-
const textChannels = guild.channels.cache.filter((ch) => ch.isTextBased()).size;
36-
const voiceChannels = guild.channels.cache.filter((ch) => ch.isVoiceBased()).size;
37-
const categories = guild.channels.cache.filter((ch) => ch.type === 4).size; // CategoryChannel type
38-
const activeThreads = guild.channels.cache.filter((ch) => ch.isThread() && !ch.archived).size;
35+
const textChannels = guild.channels.cache.filter((ch) => ch.isTextBased()).size.toLocaleString();
36+
const voiceChannels = guild.channels.cache.filter((ch) => ch.isVoiceBased()).size.toLocaleString();
37+
const categories = guild.channels.cache.filter((ch) => ch.type === 4).size.toLocaleString(); // CategoryChannel type
38+
const activeThreads = guild.channels.cache.filter((ch) => ch.isThread() && !ch.archived).size.toLocaleString();
39+
const stickerCount = guild.stickers.cache.size.toLocaleString();
3940

4041
const features =
4142
guild.features.length > 0
@@ -50,7 +51,7 @@ const formatServerInfo = (guild: Guild, detailed: boolean = false): string => {
5051
`**Categories:** ${categories}`,
5152
`**Active Threads:** ${activeThreads}`,
5253
`**Custom Emojis:** ${emojiCount}`,
53-
`**Stickers:** ${guild.stickers.cache.size}`,
54+
`**Stickers:** ${stickerCount}`,
5455
'',
5556
`🎯 **Server Features**`,
5657
`**Verification Level:** ${guild.verificationLevel}`,
@@ -108,15 +109,16 @@ export const serverInfo: Action = {
108109

109110
try {
110111
const room = state.data?.room || (await runtime.getRoom(message.roomId));
111-
if (!room?.serverId) {
112+
const serverId = room?.serverId ?? room?.messageServerId;
113+
if (!serverId) {
112114
await callback({
113115
text: "I couldn't determine the current server.",
114116
source: 'discord',
115117
});
116118
return;
117119
}
118120

119-
const guild = await discordService.client.guilds.fetch(room.serverId);
121+
const guild = await discordService.client.guilds.fetch(serverId);
120122

121123
// Check if the request is for detailed info
122124
const messageText = message.content.text?.toLowerCase() || '';

src/banner.ts

Lines changed: 223 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,234 @@
11
/**
22
* Discord Plugin Settings Banner
33
* Beautiful ANSI art display for configuration on startup
4+
* Includes tiered permission system for invite URLs
45
*/
56

67
import type { IAgentRuntime } from '@elizaos/core';
8+
import { getPermissionValues, type DiscordPermissionValues } from './permissions';
79

8-
const colors = {
9-
reset: '\x1b[0m',
10-
bright: '\x1b[1m',
11-
dim: '\x1b[2m',
12-
yellow: '\x1b[33m',
13-
cyan: '\x1b[36m',
14-
white: '\x1b[37m',
15-
brightCyan: '\x1b[96m',
16-
brightBlue: '\x1b[94m',
17-
brightWhite: '\x1b[97m',
18-
green: '\x1b[32m',
10+
const ANSI = {
11+
reset: '\x1b[0m',
12+
bold: '\x1b[1m',
13+
dim: '\x1b[2m',
14+
blue: '\x1b[34m',
15+
brightRed: '\x1b[91m',
16+
brightGreen: '\x1b[92m',
17+
brightYellow: '\x1b[93m',
18+
brightBlue: '\x1b[94m',
19+
brightMagenta: '\x1b[95m',
20+
brightCyan: '\x1b[96m',
21+
brightWhite: '\x1b[97m',
1922
};
2023

21-
export function printDiscordBanner(runtime: IAgentRuntime): void {
22-
// Get settings
23-
const apiToken = runtime.getSetting('DISCORD_API_TOKEN');
24-
const ignoreBots = runtime.getSetting('DISCORD_SHOULD_IGNORE_BOT_MESSAGES');
25-
const ignoreDMs = runtime.getSetting('DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES');
26-
const onlyMentions = runtime.getSetting('DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS');
27-
const listenChannels = runtime.getSetting('DISCORD_LISTEN_CHANNEL_IDS');
28-
const voiceChannelId = runtime.getSetting('DISCORD_VOICE_CHANNEL_ID');
29-
30-
// Check defaults
31-
const ignoreBotsDefault = ignoreBots === undefined;
32-
const ignoreDMsDefault = ignoreDMs === undefined;
33-
const onlyMentionsDefault = onlyMentions === undefined;
34-
35-
const banner = `
36-
${colors.brightBlue}================================================================================
37-
${colors.brightBlue} ____ ___ ____ ____ ____ ____ ____ ____ _ _ _ ____ ___ _ _
38-
${colors.brightCyan} | _ \\|_ _|/ ___| / ___/ _ \\| _ \\| _ \\ | _ \\| | | | | |/ ___|_ _| \\ | |
39-
${colors.brightCyan} | | | || | \\___ \\| | | | | | |_) | | | | | |_) | | | | | | | _ | || \\| |
40-
${colors.brightBlue} | |_| || | ___) | |__| |_| | _ <| |_| | | __/| |___| |_| | |_| || || |\\ |
41-
${colors.brightBlue} |____/|___||____/ \\____\\___/|_| \\_\\____/ |_| |_____|\\___/ \\____|___|_| \\_|
42-
${colors.brightBlue}================================================================================${colors.reset}
43-
44-
${colors.cyan}Configuration:${colors.reset}
45-
${colors.yellow}DISCORD_API_TOKEN${colors.reset} = ${apiToken ? colors.green + '***set***' : colors.dim + 'not set'} ${apiToken ? colors.green + '(configured)' : colors.dim + '(required)'}${colors.reset}
46-
${colors.yellow}DISCORD_SHOULD_IGNORE_BOT_MESSAGES${colors.reset} = ${colors.white}${ignoreBots || 'false'}${ignoreBotsDefault ? colors.dim + '*' : ''}${colors.reset} ${ignoreBotsDefault ? colors.dim + '(default)' : colors.green + '(set)'}${colors.reset}
47-
${colors.yellow}DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES${colors.reset} = ${colors.white}${ignoreDMs || 'false'}${ignoreDMsDefault ? colors.dim + '*' : ''}${colors.reset} ${ignoreDMsDefault ? colors.dim + '(default)' : colors.green + '(set)'}${colors.reset}
48-
${colors.yellow}DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS${colors.reset} = ${colors.white}${onlyMentions || 'false'}${onlyMentionsDefault ? colors.dim + '*' : ''}${colors.reset} ${onlyMentionsDefault ? colors.dim + '(default)' : colors.green + '(set)'}${colors.reset}
49-
${colors.yellow}DISCORD_LISTEN_CHANNEL_IDS${colors.reset} = ${listenChannels ? colors.white + 'configured' + colors.reset + ' ' + colors.green + '(set)' : colors.dim + 'not set (optional)'}${colors.reset}
50-
${colors.yellow}DISCORD_VOICE_CHANNEL_ID${colors.reset} = ${voiceChannelId ? colors.white + 'configured' + colors.reset + ' ' + colors.green + '(set)' : colors.dim + 'not set (optional)'}${colors.reset}
51-
52-
${colors.dim}* = default value | Configure via .env file${colors.reset}
53-
${colors.brightBlue}================================================================================${colors.reset}
54-
`;
55-
56-
// Use logger.info with source context for proper log formatting
57-
runtime.logger.info({ src: 'plugin:discord', agentId: runtime.agentId }, `\n${banner}\n`);
24+
export interface PluginSetting {
25+
name: string;
26+
value: unknown;
27+
defaultValue?: unknown;
28+
sensitive?: boolean;
29+
required?: boolean;
30+
}
31+
32+
export interface BannerOptions {
33+
pluginName: string;
34+
description?: string;
35+
settings: PluginSetting[];
36+
runtime: IAgentRuntime;
37+
/** Discord Application ID for generating invite URLs */
38+
applicationId?: string;
39+
/** Permission values for the 3x2 tier matrix */
40+
discordPermissions?: DiscordPermissionValues;
41+
/** @deprecated Use applicationId + discordPermissions instead */
42+
discordInviteLink?: string;
43+
}
44+
45+
function mask(v: string): string {
46+
if (!v || v.length <= 8) return '••••••••';
47+
return `${v.slice(0, 4)}${'•'.repeat(Math.min(12, v.length - 8))}${v.slice(-4)}`;
48+
}
49+
50+
function fmtVal(value: unknown, sensitive: boolean, maxLen: number): string {
51+
let s: string;
52+
if (value === undefined || value === null || value === '') {
53+
s = '(not set)';
54+
} else if (sensitive) {
55+
s = mask(String(value));
56+
} else {
57+
s = String(value);
58+
}
59+
if (s.length > maxLen) s = s.slice(0, maxLen - 3) + '...';
60+
return s;
61+
}
62+
63+
function isDef(v: unknown, d: unknown): boolean {
64+
if (v === undefined || v === null || v === '') return true;
65+
return d !== undefined && v === d;
66+
}
67+
68+
function pad(s: string, n: number): string {
69+
const len = s.replace(/\x1b\[[0-9;]*m/g, '').length;
70+
if (len >= n) return s;
71+
return s + ' '.repeat(n - len);
72+
}
73+
74+
function line(content: string): string {
75+
const ansiPattern = /\x1b\[[0-9;]*m/g;
76+
const len = content.replace(ansiPattern, '').length;
77+
78+
if (len <= 78) {
79+
return content + ' '.repeat(78 - len);
80+
}
81+
82+
// Truncate based on visible character count, not raw string position
83+
// This avoids cutting in the middle of ANSI escape sequences
84+
let visibleCount = 0;
85+
let result = '';
86+
let i = 0;
87+
88+
while (i < content.length && visibleCount < 78) {
89+
const remaining = content.slice(i);
90+
const match = remaining.match(/^\x1b\[[0-9;]*m/);
91+
92+
if (match) {
93+
// Include ANSI sequence without counting toward visible length
94+
result += match[0];
95+
i += match[0].length;
96+
} else {
97+
// Regular visible character
98+
result += content[i];
99+
visibleCount++;
100+
i++;
101+
}
102+
}
103+
104+
// Reset any unclosed ANSI sequences after truncation
105+
return result + ANSI.reset;
106+
}
107+
108+
/**
109+
* Print the Discord plugin settings banner with tiered invite URLs
110+
*/
111+
export function printBanner(options: BannerOptions): void {
112+
const { settings, runtime } = options;
113+
const R = ANSI.reset,
114+
D = ANSI.dim,
115+
B = ANSI.bold;
116+
const c1 = ANSI.brightBlue,
117+
c2 = ANSI.brightCyan,
118+
c3 = ANSI.brightMagenta;
119+
120+
const top = `${c1}${'═'.repeat(78)}${R}`;
121+
const mid = `${c1}${'═'.repeat(78)}${R}`;
122+
const bot = `${c1}${'═'.repeat(78)}${R}`;
123+
const row = (s: string) => `${c1}${R}${line(s)}${c1}${R}`;
124+
125+
const lines: string[] = [''];
126+
lines.push(top);
127+
lines.push(row(` ${B}Character: ${runtime.character.name}${R}`));
128+
lines.push(mid);
129+
lines.push(row(`${c2} ██████╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ${c3}◖ ◗${R}`));
130+
lines.push(row(`${c2} ██╔══██╗██║██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗ ${c3}◖===◗${R}`));
131+
lines.push(row(`${c2} ██║ ██║██║███████╗██║ ██║ ██║██████╔╝██║ ██║ ${c3}╰─╯${R}`));
132+
lines.push(row(`${c2} ██████╔╝██║╚════██║╚██████╗╚██████╔╝██║ ██║██████╔╝ ${c3}(◠◠)${R}`));
133+
lines.push(row(`${c2} ╚═════╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ${c3}‿‿${R}`));
134+
lines.push(row(`${D} Bot Integration • Servers • Channels • Voice${R}`));
135+
lines.push(mid);
136+
137+
const NW = 34,
138+
VW = 26,
139+
SW = 8;
140+
lines.push(row(` ${B}${pad('ENV VARIABLE', NW)} ${pad('VALUE', VW)} ${pad('STATUS', SW)}${R}`));
141+
lines.push(row(` ${D}${'-'.repeat(NW)} ${'-'.repeat(VW)} ${'-'.repeat(SW)}${R}`));
142+
143+
for (const s of settings) {
144+
const def = isDef(s.value, s.defaultValue);
145+
const set = s.value !== undefined && s.value !== null && s.value !== '';
146+
147+
let ico: string, st: string;
148+
if (!set && s.required) {
149+
ico = `${ANSI.brightRed}${R}`;
150+
st = `${ANSI.brightRed}REQUIRED${R}`;
151+
} else if (!set) {
152+
ico = `${D}${R}`;
153+
st = `${D}default${R}`;
154+
} else if (def) {
155+
ico = `${ANSI.brightBlue}${R}`;
156+
st = `${ANSI.brightBlue}default${R}`;
157+
} else {
158+
ico = `${ANSI.brightGreen}${R}`;
159+
st = `${ANSI.brightGreen}custom${R}`;
160+
}
161+
162+
const name = pad(s.name, NW - 2);
163+
const val = pad(fmtVal(s.value ?? s.defaultValue, s.sensitive ?? false, VW), VW);
164+
const status = pad(st, SW);
165+
lines.push(row(` ${ico} ${c2}${name}${R} ${val} ${status}`));
166+
}
167+
168+
lines.push(mid);
169+
lines.push(
170+
row(
171+
` ${D}${ANSI.brightGreen}${D} custom ${ANSI.brightBlue}${D} default ○ unset ${ANSI.brightRed}${D} required → Set in .env${R}`
172+
)
173+
);
174+
lines.push(bot);
175+
176+
// Add Discord invite links organized by voice capability
177+
if (options.applicationId && options.discordPermissions) {
178+
const p = options.discordPermissions;
179+
const baseUrl = `https://discord.com/api/oauth2/authorize?client_id=${options.applicationId}&scope=bot%20applications.commands&permissions=`;
180+
181+
lines.push('');
182+
lines.push(`${B}${ANSI.brightCyan}🔗 Discord Bot Invite${R}`);
183+
lines.push('');
184+
lines.push(` ${B}🎙️ With Voice:${R}`);
185+
lines.push(` ${ANSI.brightGreen}● Basic${R} ${baseUrl}${p.basicVoice}`);
186+
lines.push(` ${ANSI.brightYellow}● Moderator${R} ${baseUrl}${p.moderatorVoice}`);
187+
lines.push(` ${ANSI.brightRed}● Admin${R} ${baseUrl}${p.adminVoice}`);
188+
lines.push('');
189+
lines.push(` ${B}💬 Without Voice:${R}`);
190+
lines.push(` ${ANSI.brightCyan}○ Basic${R} ${baseUrl}${p.basic}`);
191+
lines.push(` ${ANSI.brightMagenta}○ Moderator${R} ${baseUrl}${p.moderator}`);
192+
lines.push(` ${ANSI.brightBlue}○ Admin${R} ${baseUrl}${p.admin}`);
193+
} else if (options.discordInviteLink) {
194+
// Backwards compatibility
195+
lines.push('');
196+
lines.push(`${B}${ANSI.brightCyan}🔗 Discord Bot Invite:${R} ${options.discordInviteLink}`);
197+
}
198+
199+
lines.push('');
200+
201+
runtime.logger.info(lines.join('\n'));
58202
}
59203

204+
/**
205+
* Simple banner for backwards compatibility
206+
* @deprecated Use printBanner with BannerOptions instead
207+
*/
208+
export function printDiscordBanner(runtime: IAgentRuntime): void {
209+
// Get settings
210+
const apiToken = runtime.getSetting('DISCORD_API_TOKEN');
211+
const applicationId = runtime.getSetting('DISCORD_APPLICATION_ID');
212+
const ignoreBots = runtime.getSetting('DISCORD_SHOULD_IGNORE_BOT_MESSAGES');
213+
const ignoreDMs = runtime.getSetting('DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES');
214+
const onlyMentions = runtime.getSetting('DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS');
215+
const listenChannels = runtime.getSetting('DISCORD_LISTEN_CHANNEL_IDS');
216+
const voiceChannelId = runtime.getSetting('DISCORD_VOICE_CHANNEL_ID');
217+
218+
printBanner({
219+
pluginName: 'plugin-discord',
220+
description: 'Discord bot integration for servers and channels',
221+
applicationId: applicationId || undefined,
222+
discordPermissions: applicationId ? getPermissionValues() : undefined,
223+
settings: [
224+
{ name: 'DISCORD_API_TOKEN', value: apiToken, sensitive: true, required: true },
225+
{ name: 'DISCORD_APPLICATION_ID', value: applicationId },
226+
{ name: 'DISCORD_VOICE_CHANNEL_ID', value: voiceChannelId },
227+
{ name: 'DISCORD_LISTEN_CHANNEL_IDS', value: listenChannels },
228+
{ name: 'DISCORD_SHOULD_IGNORE_BOT_MESSAGES', value: ignoreBots, defaultValue: 'false' },
229+
{ name: 'DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES', value: ignoreDMs, defaultValue: 'false' },
230+
{ name: 'DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS', value: onlyMentions, defaultValue: 'false' },
231+
],
232+
runtime,
233+
});
234+
}

0 commit comments

Comments
 (0)