2025-05-04 18:26:56 +00:00
import { SlashCommandBuilder , PermissionFlagsBits , ChannelType , EmbedBuilder } from 'discord.js' ;
import { MessageFlags } from 'discord-api-types/v10' ;
2025-05-05 17:48:42 +00:00
// Init function to handle autocomplete for /vc invite
2025-05-04 18:26:56 +00:00
/ * *
* tempvc module : temporary voice channel manager
*
* Admin commands ( / v c a d m i n ) :
* - add < voice _channel > < category >
* - remove < voice _channel >
* - list
*
* User commands ( / v c ) :
* 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' )
2025-05-05 17:48:42 +00:00
// Autocomplete string option for user ID
. addStringOption ( opt =>
opt . setName ( 'user' )
. setDescription ( 'User to invite' )
. setRequired ( true )
. setAutocomplete ( true )
)
2025-05-04 18:26:56 +00:00
)
. 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' ) {
2025-05-05 17:48:42 +00:00
// Invitation: support both string (autocomplete) and user option types
let userId ;
let memberToInvite ;
// Try string option first (autocomplete)
try {
userId = interaction . options . getString ( 'user' , true ) ;
memberToInvite = await guild . members . fetch ( userId ) ;
} catch ( e ) {
// Fallback to user option
try {
const user = interaction . options . getUser ( 'user' , true ) ;
userId = user . id ;
memberToInvite = await guild . members . fetch ( userId ) ;
} catch {
memberToInvite = null ;
}
}
if ( ! memberToInvite ) {
return interaction . reply ( { content : 'User not found in this server.' , flags : MessageFlags . Ephemeral } ) ;
}
2025-05-04 18:41:46 +00:00
// grant view and connect
2025-05-05 17:48:42 +00:00
await voice . permissionOverwrites . edit ( userId , { ViewChannel : true , Connect : true } ) ;
await interaction . reply ( { content : ` Invited <@ ${ userId } >. ` , flags : MessageFlags . Ephemeral } ) ;
2025-05-04 18:26:56 +00:00
} 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 ) ;
2025-05-04 18:41:46 +00:00
// clear all overwrites: allow everyone, owner elevated perms
2025-05-04 18:26:56 +00:00
await voice . permissionOverwrites . set ( [
2025-05-04 18:41:46 +00:00
{ id : guild . roles . everyone . id , allow : [ PermissionFlagsBits . ViewChannel , PermissionFlagsBits . Connect ] } ,
{ id : sess . ownerId , allow : [
PermissionFlagsBits . ViewChannel ,
PermissionFlagsBits . Connect ,
PermissionFlagsBits . MoveMembers ,
PermissionFlagsBits . ManageChannels ,
PermissionFlagsBits . PrioritySpeaker ,
PermissionFlagsBits . MuteMembers ,
PermissionFlagsBits . DeafenMembers
]
}
2025-05-04 18:26:56 +00:00
] ) ;
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' ) {
2025-05-04 18:41:46 +00:00
// only allow whitelisted role
await voice . permissionOverwrites . edit ( guild . roles . everyone . id , { ViewChannel : false } ) ;
if ( sess . roleId ) await voice . permissionOverwrites . edit ( sess . roleId , { ViewChannel : true } ) ;
2025-05-04 18:26:56 +00:00
} else {
// blacklist: allow everyone, then deny the specified role
2025-05-04 18:41:46 +00:00
await voice . permissionOverwrites . edit ( guild . roles . everyone . id , { ViewChannel : true } ) ;
if ( sess . roleId ) await voice . permissionOverwrites . edit ( sess . roleId , { ViewChannel : false } ) ;
2025-05-04 18:26:56 +00:00
}
// 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 ;
2025-05-04 18:41:46 +00:00
// adjust view/connect for @everyone
await voice . permissionOverwrites . edit (
guild . roles . everyone . id ,
{ ViewChannel : mode === 'blacklist' , Connect : mode === 'blacklist' }
) ;
// adjust view/connect for role
2025-05-04 18:26:56 +00:00
if ( preset . roleId ) {
2025-05-04 18:41:46 +00:00
await voice . permissionOverwrites . edit (
preset . roleId ,
{ ViewChannel : mode === 'whitelist' , Connect : mode === 'whitelist' }
) ;
2025-05-04 18:26:56 +00:00
}
// 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 ) {
2025-05-05 17:48:42 +00:00
// autocomplete for /vc invite
client . on ( 'interactionCreate' , async interaction => {
if ( ! interaction . isAutocomplete ( ) ) return ;
if ( interaction . commandName !== 'vc' ) return ;
// Only handle autocomplete for the 'invite' subcommand
let sub ;
try {
sub = interaction . options . getSubcommand ( ) ;
} catch {
return ;
}
if ( sub !== 'invite' ) return ;
const focused = interaction . options . getFocused ( ) ;
const guild = interaction . guild ;
if ( ! guild ) return ;
// Perform guild member search for autocomplete suggestions (prefix match)
let choices = [ ] ;
try {
const members = await guild . members . search ( { query : focused , limit : 25 } ) ;
choices = members . map ( m => ( { name : m . displayName || m . user . username , value : m . id } ) ) ;
} catch ( err ) {
client . logger . error ( ` [module:tempvc] Autocomplete search failed: ${ err . message } ` ) ;
}
// If no choices found or to support substring matching, fallback to cache filter
if ( choices . length === 0 ) {
const str = String ( focused ) . toLowerCase ( ) ;
choices = Array . from ( guild . members . cache . values ( ) )
. filter ( m => ( m . displayName || m . user . username ) . toLowerCase ( ) . includes ( str ) )
. slice ( 0 , 25 )
. map ( m => ( { name : m . displayName || m . user . username , value : m . id } ) ) ;
}
// Respond with suggestions (max 25)
await interaction . respond ( choices ) ;
} ) ;
2025-05-04 18:26:56 +00:00
// 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)
2025-05-04 18:41:46 +00:00
// create voice channel; default allow everyone view/join, owner elevated perms
2025-05-04 18:26:56 +00:00
const ch = await guild . channels . create ( {
name ,
type : ChannelType . GuildVoice ,
parent : catId ,
permissionOverwrites : [
2025-05-04 18:41:46 +00:00
{ id : guild . roles . everyone . id , allow : [ PermissionFlagsBits . ViewChannel , PermissionFlagsBits . Connect ] } ,
{ id : owner . id , allow : [
PermissionFlagsBits . ViewChannel ,
PermissionFlagsBits . Connect ,
PermissionFlagsBits . MoveMembers ,
PermissionFlagsBits . ManageChannels ,
PermissionFlagsBits . PrioritySpeaker ,
PermissionFlagsBits . MuteMembers ,
PermissionFlagsBits . DeafenMembers
]
}
2025-05-04 18:26:56 +00:00
]
} ) ;
// 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' ) ;
}