585 lines
26 KiB
JavaScript
585 lines
26 KiB
JavaScript
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 <voice_channel> <category>
|
||
* - remove <voice_channel>
|
||
* - list
|
||
*
|
||
* User commands (/vc):
|
||
* Access Control:
|
||
* • invite <user>
|
||
* • kick <user>
|
||
* • role <role>
|
||
* • mode <whitelist|blacklist>
|
||
* • limit <0-99>
|
||
* Presets:
|
||
* • save <name>
|
||
* • restore <name>
|
||
* • reset
|
||
* Utilities:
|
||
* • rename <new_name>
|
||
* • 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 <user> — Invite a user to this channel\n' +
|
||
'• /vc kick <user> — Kick a user from this channel\n' +
|
||
'• /vc role <role> — Set a role to allow/deny access\n' +
|
||
'• /vc mode <whitelist|blacklist> — Switch role mode\n' +
|
||
'• /vc limit <number> — Set user limit (0–99)',
|
||
},
|
||
{
|
||
name: 'Presets',
|
||
value:
|
||
'• /vc save <name> — Save current settings as a preset\n' +
|
||
'• /vc restore <name> — Restore settings from a preset\n' +
|
||
'• /vc reset — Reset channel to default settings',
|
||
},
|
||
{
|
||
name: 'Utilities',
|
||
value:
|
||
'• /vc rename <new_name> — 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');
|
||
} |