From 10d8a0b900af99646106598e3bbfd03254f2e4e0 Mon Sep 17 00:00:00 2001 From: jrmyr Date: Sun, 4 May 2025 18:26:56 +0000 Subject: [PATCH] TempVC module added. --- _opt/tempvc.js | 585 +++++++++++++++++++++++++++++++++++++++++++++++++ config.js | 5 +- index.js | 5 +- 3 files changed, 591 insertions(+), 4 deletions(-) create mode 100644 _opt/tempvc.js diff --git a/_opt/tempvc.js b/_opt/tempvc.js new file mode 100644 index 0000000..5147212 --- /dev/null +++ b/_opt/tempvc.js @@ -0,0 +1,585 @@ +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js'; +import { MessageFlags } from 'discord-api-types/v10'; +/** + * tempvc module: temporary voice channel manager + * + * Admin commands (/vcadmin): + * - add + * - remove + * - list + * + * User commands (/vc): + * Access Control: + * • invite + * • kick + * • role + * • mode + * • limit <0-99> + * Presets: + * • save + * • restore + * • reset + * Utilities: + * • rename + * • info + * • delete + * + * PocketBase collections required: + * tempvc_masters (guildId, masterChannelId, categoryId) + * tempvc_sessions (guildId, masterChannelId, channelId, ownerId, roleId, mode) + * tempvc_presets (guildId, userId, name, channelName, userLimit, roleId, invitedUserIds, mode) + */ + +// Temporary Voice Channel module +// - /vcadmin: admin commands to add/remove/list spawn channels +// - /vc: user commands to manage own temp VC, presets save/restore + +/** + * Slash commands for vcadmin and vc + */ +export const commands = [ + // Administrator: manage spawn points + { + data: new SlashCommandBuilder() + .setName('vcadmin') + .setDescription('Configure temporary voice-channel spawn points (Admin only)') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addSubcommand(sub => + sub.setName('add') + .setDescription('Add a spawn voice channel and its temp category') + .addChannelOption(opt => + opt.setName('voice_channel') + .setDescription('Voice channel to spawn from') + .setRequired(true) + .addChannelTypes(ChannelType.GuildVoice) + ) + .addChannelOption(opt => + opt.setName('category') + .setDescription('Category for new temp channels') + .setRequired(true) + .addChannelTypes(ChannelType.GuildCategory) + ) + ) + .addSubcommand(sub => + sub.setName('remove') + .setDescription('Remove a spawn voice channel') + .addChannelOption(opt => + opt.setName('voice_channel') + .setDescription('Voice channel to remove') + .setRequired(true) + .addChannelTypes(ChannelType.GuildVoice) + ) + ) + .addSubcommand(sub => + sub.setName('list') + .setDescription('List all spawn voice channels and categories') + ), + async execute(interaction, client) { + const guildId = interaction.guildId; + const sub = interaction.options.getSubcommand(); + // ensure in-guild + if (!guildId) { + return interaction.reply({ content: 'This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + } + // init memory map for this guild + client.tempvc = client.tempvc || { masters: new Map(), sessions: new Map() }; + if (!client.tempvc.masters.has(guildId)) { + client.tempvc.masters.set(guildId, new Map()); + } + const guildMasters = client.tempvc.masters.get(guildId); + try { + if (sub === 'add') { + const vc = interaction.options.getChannel('voice_channel', true); + const cat = interaction.options.getChannel('category', true); + // persist + const existing = await client.pb.getFirst( + 'tempvc_masters', + `guildId = "${guildId}" && masterChannelId = "${vc.id}"` + ); + if (existing) { + await client.pb.updateOne('tempvc_masters', existing.id, { + guildId, masterChannelId: vc.id, categoryId: cat.id + }); + } else { + await client.pb.createOne('tempvc_masters', { + guildId, masterChannelId: vc.id, categoryId: cat.id + }); + } + // update memory + guildMasters.set(vc.id, cat.id); + await interaction.reply({ + content: `Spawn channel <#${vc.id}> will now create temp VCs in <#${cat.id}>.`, + flags: MessageFlags.Ephemeral + }); + } else if (sub === 'remove') { + const vc = interaction.options.getChannel('voice_channel', true); + if (!guildMasters.has(vc.id)) { + return interaction.reply({ content: 'That channel is not configured as a spawn point.', flags: MessageFlags.Ephemeral }); + } + // remove from PB + const existing = await client.pb.getFirst( + 'tempvc_masters', + `guildId = "${guildId}" && masterChannelId = "${vc.id}"` + ); + if (existing) { + await client.pb.deleteOne('tempvc_masters', existing.id); + } + // update memory + guildMasters.delete(vc.id); + await interaction.reply({ content: `Removed spawn channel <#${vc.id}>.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'list') { + if (guildMasters.size === 0) { + return interaction.reply({ content: 'No spawn channels configured.', flags: MessageFlags.Ephemeral }); + } + const lines = []; + for (const [mId, cId] of guildMasters.entries()) { + lines.push(`<#${mId}> → <#${cId}>`); + } + await interaction.reply({ content: '**Spawn channels:**\n' + lines.join('\n'), flags: MessageFlags.Ephemeral }); + } + } catch (err) { + client.logger.error(`[module:tempvc][vcadmin] ${err.message}`); + await interaction.reply({ content: 'Operation failed, see logs.', flags: MessageFlags.Ephemeral }); + } + } + }, + // User: manage own temp VC and presets + { + data: new SlashCommandBuilder() + .setName('vc') + .setDescription('Manage your temporary voice channel') + .setDMPermission(false) + // Access Control + .addSubcommand(sub => + sub.setName('invite') + .setDescription('Invite a user to this channel') + .addUserOption(opt => opt.setName('user').setDescription('User to invite').setRequired(true)) + ) + .addSubcommand(sub => + sub.setName('kick') + .setDescription('Kick a user from this channel') + .addUserOption(opt => opt.setName('user').setDescription('User to kick').setRequired(true)) + ) + .addSubcommand(sub => + sub.setName('role') + .setDescription('Set role to allow/deny access') + .addRoleOption(opt => opt.setName('role').setDescription('Role to allow/deny').setRequired(true)) + ) + .addSubcommand(sub => + sub.setName('mode') + .setDescription('Switch role mode') + .addStringOption(opt => + opt.setName('mode') + .setDescription('Mode: whitelist or blacklist') + .setRequired(true) + .addChoices( + { name: 'whitelist', value: 'whitelist' }, + { name: 'blacklist', value: 'blacklist' } + ) + ) + ) + .addSubcommand(sub => + sub.setName('limit') + .setDescription('Set user limit (0–99)') + .addIntegerOption(opt => opt.setName('number').setDescription('Max users').setRequired(true)) + ) + // Presets + .addSubcommand(sub => + sub.setName('save') + .setDescription('Save current settings as a preset') + .addStringOption(opt => opt.setName('name').setDescription('Preset name').setRequired(true).setAutocomplete(true)) + ) + .addSubcommand(sub => + sub.setName('restore') + .setDescription('Restore settings from a preset') + .addStringOption(opt => opt.setName('name').setDescription('Preset name').setRequired(true).setAutocomplete(true)) + ) + .addSubcommand(sub => sub.setName('reset').setDescription('Reset channel to default settings')) + // Utilities + .addSubcommand(sub => sub.setName('rename').setDescription('Rename this channel').addStringOption(opt => opt.setName('new_name').setDescription('New channel name').setRequired(true))) + .addSubcommand(sub => sub.setName('info').setDescription('Show channel info')) + .addSubcommand(sub => sub.setName('delete').setDescription('Delete this channel')), + async execute(interaction, client) { + const guild = interaction.guild; + const member = interaction.member; + const sub = interaction.options.getSubcommand(); + // must be in guild and in voice + if (!guild || !member || !member.voice.channel) { + return interaction.reply({ content: 'You must be in a temp voice channel to use this.', flags: MessageFlags.Ephemeral }); + } + const voice = member.voice.channel; + client.tempvc = client.tempvc || { masters: new Map(), sessions: new Map() }; + const sess = client.tempvc.sessions.get(voice.id); + if (!sess) { + return interaction.reply({ content: 'This is not one of my temporary channels.', flags: MessageFlags.Ephemeral }); + } + if (sess.ownerId !== interaction.user.id) { + return interaction.reply({ content: 'Only the room owner can do that.', flags: MessageFlags.Ephemeral }); + } + try { + if (sub === 'rename') { + const name = interaction.options.getString('new_name', true); + await voice.setName(name); + await interaction.reply({ content: `Channel renamed to **${name}**.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'invite') { + const u = interaction.options.getUser('user', true); + await voice.permissionOverwrites.edit(u.id, { Connect: true }); + await interaction.reply({ content: `Invited <@${u.id}>.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'kick') { + const u = interaction.options.getUser('user', true); + const gm = await guild.members.fetch(u.id); + // move them out if in this channel + if (gm.voice.channelId === voice.id) { + await gm.voice.setChannel(null); + } + // remove any previous invite allow + try { + await voice.permissionOverwrites.delete(u.id); + } catch {} + await interaction.reply({ content: `Kicked <@${u.id}>.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'limit') { + let num = interaction.options.getInteger('number', true); + // enforce range 0-99 + if (num < 0 || num > 99) { + return interaction.reply({ content: 'User limit must be between 0 (no limit) and 99.', flags: MessageFlags.Ephemeral }); + } + await voice.setUserLimit(num); + await interaction.reply({ content: `User limit set to ${num}.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'role') { + const newRole = interaction.options.getRole('role', true); + const oldRoleId = sess.roleId; + // remove old role overwrite if any + if (oldRoleId && oldRoleId !== guild.roles.everyone.id) { + await voice.permissionOverwrites.delete(oldRoleId).catch(() => {}); + } + // selecting @everyone resets all + if (newRole.id === guild.roles.everyone.id) { + // clear all overwrites + await voice.permissionOverwrites.set([ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.Connect] }, + { id: sess.ownerId, allow: [PermissionFlagsBits.Connect, PermissionFlagsBits.MoveMembers, PermissionFlagsBits.ManageChannels] } + ]); + sess.roleId = ''; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: '', mode: sess.mode }); + return interaction.reply({ content: '@everyone can now connect.', flags: MessageFlags.Ephemeral }); + } + if (sess.mode === 'whitelist') { + // whitelist: lock everyone, allow role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: false }); + await voice.permissionOverwrites.edit(newRole.id, { Connect: true }); + sess.roleId = newRole.id; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: newRole.id, mode: sess.mode }); + await interaction.reply({ content: `Whitelisted role <@&${newRole.id}>.`, flags: MessageFlags.Ephemeral }); + } else { + // blacklist: allow everyone, deny role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: true }); + await voice.permissionOverwrites.edit(newRole.id, { Connect: false }); + sess.roleId = newRole.id; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: newRole.id, mode: sess.mode }); + await interaction.reply({ content: `Blacklisted role <@&${newRole.id}>.`, flags: MessageFlags.Ephemeral }); + } + } else if (sub === 'delete') { + await interaction.reply({ content: 'Deleting your channel...', flags: MessageFlags.Ephemeral }); + await client.pb.deleteOne('tempvc_sessions', sess.pbId); + client.tempvc.sessions.delete(voice.id); + await voice.delete('Owner deleted temp VC'); + } else if (sub === 'info') { + const invites = voice.permissionOverwrites.cache + .filter(po => po.allow.has(PermissionFlagsBits.Connect) && ![guild.roles.everyone.id, sess.roleId].includes(po.id)) + .map(po => `<@${po.id}>`); + const everyoneId = guild.roles.everyone.id; + const roleLine = (!sess.roleId || sess.roleId === everyoneId) + ? '@everyone' + : `<@&${sess.roleId}>`; + const modeLine = sess.mode || 'whitelist'; + const lines = [ + `Owner: <@${sess.ownerId}>`, + `Name: ${voice.name}`, + `Role: ${roleLine} (${modeLine})`, + `User limit: ${voice.userLimit}`, + `Invites: ${invites.length ? invites.join(', ') : 'none'}` + ]; + await interaction.reply({ content: lines.join('\n'), flags: MessageFlags.Ephemeral }); + } else if (sub === 'save') { + const name = interaction.options.getString('name', true); + // gather invites + const invited = voice.permissionOverwrites.cache + .filter(po => po.allow.has(PermissionFlagsBits.Connect) && ![guild.roles.everyone.id, sess.roleId].includes(po.id)) + .map(po => po.id); + // upsert preset + const existing = await client.pb.getFirst( + 'tempvc_presets', + `guildId = "${guild.id}" && userId = "${interaction.user.id}" && name = "${name}"` + ); + const data = { + guildId: guild.id, + userId: interaction.user.id, + name, + channelName: voice.name, + userLimit: voice.userLimit, + roleId: sess.roleId || '', + invitedUserIds: invited, + mode: sess.mode || 'whitelist' + }; + if (existing) { + await client.pb.updateOne('tempvc_presets', existing.id, data); + } else { + await client.pb.createOne('tempvc_presets', data); + } + await interaction.reply({ content: `Preset **${name}** saved.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'reset') { + // reset channel to default parameters + const owner = interaction.member; + const display = owner.displayName || owner.user.username; + const defaultName = `TempVC: ${display}`; + await voice.setName(defaultName); + await voice.setUserLimit(0); + // clear all overwrites except owner, default allow @everyone + await voice.permissionOverwrites.set([ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.Connect] }, + { id: sess.ownerId, allow: [PermissionFlagsBits.Connect, PermissionFlagsBits.MoveMembers, PermissionFlagsBits.ManageChannels] } + ]); + sess.roleId = guild.roles.everyone.id; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: guild.roles.everyone.id, invitedUserIds: [] }); + await interaction.reply({ content: 'Channel has been reset to default settings.', flags: MessageFlags.Ephemeral }); + } else if (sub === 'mode') { + const mode = interaction.options.getString('mode', true); + sess.mode = mode; + // apply mode overwrites + if (mode === 'whitelist') { + // only allow whitelisted role + owner + invites + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: false }); + if (sess.roleId) await voice.permissionOverwrites.edit(sess.roleId, { Connect: true }); + } else { + // blacklist: allow everyone, then deny the specified role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: true }); + if (sess.roleId) await voice.permissionOverwrites.edit(sess.roleId, { Connect: false }); + } + // persist mode + await client.pb.updateOne('tempvc_sessions', sess.pbId, { mode }); + await interaction.reply({ content: `Channel mode set to **${mode}**.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'restore') { + const name = interaction.options.getString('name', true); + const preset = await client.pb.getFirst( + 'tempvc_presets', + `guildId = "${guild.id}" && userId = "${interaction.user.id}" && name = "${name}"` + ); + if (!preset) { + return interaction.reply({ content: `Preset **${name}** not found.`, flags: MessageFlags.Ephemeral }); + } + // apply settings + await voice.setName(preset.channelName); + await voice.setUserLimit(preset.userLimit); + // apply mode-based permissions + const mode = preset.mode || 'whitelist'; + sess.mode = mode; + // clear existing overwrites for everyone and role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: mode === 'blacklist' }); + if (preset.roleId) { + await voice.permissionOverwrites.edit(preset.roleId, { Connect: mode === 'whitelist' ? true : false }); + } + // invite users explicitly + for (const uid of preset.invitedUserIds || []) { + await voice.permissionOverwrites.edit(uid, { Connect: true }).catch(() => {}); + } + // persist session changes + await client.pb.updateOne( + 'tempvc_sessions', + sess.pbId, + { roleId: preset.roleId || '', mode } + ); + sess.roleId = preset.roleId || ''; + await interaction.reply({ content: `Preset **${name}** restored (mode: ${mode}).`, flags: MessageFlags.Ephemeral }); + } + } catch (err) { + client.logger.error(`[module:tempvc][vc] ${err.message}`); + await interaction.reply({ content: 'Operation failed, see logs.', flags: MessageFlags.Ephemeral }); + } + } + } +]; + +/** + * Initialize module: load PB state and hook events + */ +export async function init(client) { + // tempvc state: masters per guild, sessions map + client.tempvc = { masters: new Map(), sessions: new Map() }; + // hook voice state updates + client.on('voiceStateUpdate', async (oldState, newState) => { + client.logger.debug( + `[module:tempvc] voiceStateUpdate: user=${newState.id} oldChannel=${oldState.channelId} newChannel=${newState.channelId}` + ); + // cleanup on leave + if (oldState.channelId && oldState.channelId !== newState.channelId) { + const sess = client.tempvc.sessions.get(oldState.channelId); + const ch = oldState.guild.channels.cache.get(oldState.channelId); + if (sess && (!ch || ch.members.size === 0)) { + await client.pb.deleteOne('tempvc_sessions', sess.pbId).catch(()=>{}); + client.tempvc.sessions.delete(oldState.channelId); + await ch?.delete('Empty temp VC cleanup').catch(()=>{}); + } + } + // spawn on join + if (newState.channelId && newState.channelId !== oldState.channelId) { + const masters = client.tempvc.masters.get(newState.guild.id) || new Map(); + client.logger.debug( + `[module:tempvc] Guild ${newState.guild.id} masters: ${[...masters.keys()].join(',')}` + ); + client.logger.debug( + `[module:tempvc] Checking spawn for channel ${newState.channelId}: ${masters.has(newState.channelId)}` + ); + if (masters.has(newState.channelId)) { + const catId = masters.get(newState.channelId); + const owner = newState.member; + const guild = newState.guild; + // default channel name + const displayName = owner.displayName || owner.user.username; + const name = `TempVC: ${displayName}`; + // create channel + // create voice channel, default permissions inherited from category (allow everyone) + const ch = await guild.channels.create({ + name, + type: ChannelType.GuildVoice, + parent: catId, + permissionOverwrites: [ + { id: owner.id, allow: [PermissionFlagsBits.Connect, PermissionFlagsBits.MoveMembers, PermissionFlagsBits.ManageChannels] } + ] + }); + // move member + await owner.voice.setChannel(ch); + // persist session + const rec = await client.pb.createOne('tempvc_sessions', { + guildId: guild.id, + masterChannelId: newState.channelId, + channelId: ch.id, + ownerId: owner.id, + roleId: guild.roles.everyone.id, + mode: 'whitelist' + }); + client.tempvc.sessions.set(ch.id, { + pbId: rec.id, + guildId: guild.id, + masterChannelId: newState.channelId, + ownerId: owner.id, + roleId: guild.roles.everyone.id, + mode: 'whitelist' + }); + // send instructions to the voice channel itself + try { + const helpEmbed = new EmbedBuilder() + .setTitle('👋 Welcome to Your Temporary Voice Channel!') + .setColor('Blue') + .addFields( + { + name: 'Access Control', + value: + '• /vc invite — Invite a user to this channel\n' + + '• /vc kick — Kick a user from this channel\n' + + '• /vc role — Set a role to allow/deny access\n' + + '• /vc mode — Switch role mode\n' + + '• /vc limit — Set user limit (0–99)', + }, + { + name: 'Presets', + value: + '• /vc save — Save current settings as a preset\n' + + '• /vc restore — Restore settings from a preset\n' + + '• /vc reset — Reset channel to default settings', + }, + { + name: 'Utilities', + value: + '• /vc rename — Rename this channel\n' + + '• /vc info — Show channel info\n' + + '• /vc delete — Delete this channel', + } + ); + await ch.send({ embeds: [helpEmbed] }); + } catch (err) { + client.logger.error(`[module:tempvc] Error sending help message: ${err.message}`); + } + } + } + }); + // autocomplete for /vc save & restore presets + client.on('interactionCreate', async interaction => { + if (!interaction.isAutocomplete() || interaction.commandName !== 'vc') return; + const sub = interaction.options.getSubcommand(false); + if (!['save', 'restore'].includes(sub)) return; + const focused = interaction.options.getFocused(true); + if (focused.name !== 'name') return; + const guildId = interaction.guildId; + const userId = interaction.user.id; + try { + const recs = await client.pb.getAll('tempvc_presets', { + filter: `guildId = "${guildId}" && userId = "${userId}"` + }); + const choices = recs + .filter(r => r.name.toLowerCase().startsWith(focused.value.toLowerCase())) + .slice(0, 25) + .map(r => ({ name: r.name, value: r.name })); + await interaction.respond(choices); + } catch (err) { + client.logger.error(`[module:tempvc][autocomplete] ${err.message}`); + await interaction.respond([]); + } + }); + // On ready: load masters/sessions, then check required permissions + client.on('ready', async () => { + // Load persistent spawn masters and active sessions + for (const guild of client.guilds.cache.values()) { + const gid = guild.id; + try { + const masters = await client.pb.getAll('tempvc_masters', { filter: `guildId = "${gid}"` }); // guildId = "X" works, but escaped quotes are allowed + const gm = new Map(); + for (const rec of masters) gm.set(rec.masterChannelId, rec.categoryId); + client.tempvc.masters.set(gid, gm); + client.logger.info(`[module:tempvc] Loaded spawn masters for guild ${gid}: ${[...gm.keys()].join(', ')}`); + } catch (err) { + client.logger.error(`[module:tempvc] Error loading masters for guild ${gid}: ${err.message}`); + } + try { + const sessions = await client.pb.getAll('tempvc_sessions', { filter: `guildId = "${gid}"` }); + for (const rec of sessions) { + const ch = guild.channels.cache.get(rec.channelId); + if (ch && ch.isVoiceBased()) { + client.tempvc.sessions.set(rec.channelId, { + pbId: rec.id, + guildId: gid, + masterChannelId: rec.masterChannelId, + ownerId: rec.ownerId, + roleId: rec.roleId || '', + mode: rec.mode || 'whitelist' + }); + if (rec.roleId) await ch.permissionOverwrites.edit(rec.roleId, { Connect: true }).catch(()=>{}); + await ch.permissionOverwrites.edit(rec.ownerId, { Connect: true, ManageChannels: true, MoveMembers: true }); + } else { + await client.pb.deleteOne('tempvc_sessions', rec.id).catch(()=>{}); + } + } + } catch (err) { + client.logger.error(`[module:tempvc] Error loading sessions for guild ${gid}: ${err.message}`); + } + } + // Verify necessary permissions + for (const guild of client.guilds.cache.values()) { + // get bot's member in this guild + let me = guild.members.me; + if (!me) { + try { me = await guild.members.fetch(client.user.id); } catch { /* ignore */ } + } + if (!me) continue; + const missing = []; + if (!me.permissions.has(PermissionFlagsBits.ManageChannels)) missing.push('ManageChannels'); + if (!me.permissions.has(PermissionFlagsBits.MoveMembers)) missing.push('MoveMembers'); + if (missing.length) { + client.logger.warn( + `[module:tempvc] Missing permissions in guild ${guild.id} (${guild.name}): ${missing.join(', ')}` + ); + } + } + }); + client.logger.info('[module:tempvc] Module initialized'); +} \ No newline at end of file diff --git a/config.js b/config.js index 7357aad..60d3be6 100644 --- a/config.js +++ b/config.js @@ -72,7 +72,8 @@ export default { 'gitUtils', 'responses', 'responsesPrompt', - 'responsesQuery' + 'responsesQuery', + 'tempvc' ] }, @@ -199,7 +200,7 @@ export default { 'botUtils', 'pbUtils', 'gitUtils', - 'condimentX', + //'condimentX', 'responses', 'responsesPrompt', 'responsesQuery', diff --git a/index.js b/index.js index 38f06c6..4cdafb7 100644 --- a/index.js +++ b/index.js @@ -10,12 +10,13 @@ import { ansi, wrapAnsi } from './_src/ansiColors.js'; const initializeClient = async (clientConfig) => { // Create Discord client with intents const client = new Client({ - // Include GuildMembers intent to allow fetching all guild members + // Include GuildVoiceStates and GuildMembers intents to track voice channel events intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMembers + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildVoiceStates ] });