Stryker-SC/Discord-Bot-VCMaster-JavaScript-Revamped
0
1const { Events, ChannelType, PermissionFlagsBits, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');2const cooldowns = new Map();3 4// Global interval to sweep cooldowns (run once per hour)5setInterval(() => {6 const now = Date.now();7 for (const [userId, timestamp] of cooldowns.entries()) {8 if (now - timestamp > 30000) {9 cooldowns.delete(userId);10 }11 }12}, 60 * 60 * 1000);13 14module.exports = {15 name: Events.VoiceStateUpdate,16 async execute(oldState, newState, client) {17 // 1. Check if user Joined or Switched to a channel18 if (newState.channelId && newState.channelId !== oldState.channelId) {19 const guildConfig = await client.db.getGuildConfig(newState.guild.id);20 if (guildConfig && newState.channelId === guildConfig.hub_channel_id) {21 // Rate Limit Check22 const currentTime = Date.now();23 const lastCreation = cooldowns.get(newState.member.id);24 if (lastCreation && currentTime - lastCreation < 30000) {25 return; // Still on cooldown26 }27 28 // Apply cooldown immediately to prevent race conditions29 cooldowns.set(newState.member.id, currentTime);30 31 try {32 // User joined the Hub Channel33 await createTempChannel(newState, guildConfig, client);34 } catch (error) {35 console.error("Critical error in createTempChannel wrapper:", error);36 // Do not remove cooldown on error to prevent spam/abuse loops37 }38 }39 }40 41 // 2. Check if user Left or Switched from a channel42 if (oldState.channelId && oldState.channelId !== newState.channelId) {43 const tempChannel = await client.db.getTempChannel(oldState.channelId);44 if (tempChannel) {45 // User left a temp channel46 await checkDeleteTempChannel(oldState.channel, tempChannel, client);47 }48 }49 },50};51 52async function createTempChannel(voiceState, config, client) {53 const { guild, member } = voiceState;54 const parentId = config.category_id || voiceState.channel.parentId;55 56 // Fallback channel name since Presence intent is removed57 let channelName = `${member.displayName}'s Channel`;58 if (channelName.length > 100) channelName = channelName.substring(0, 100);59 60 let newVoiceChannel = null;61 let newTextChannel = null;62 63 try {64 // Create the Voice Channel65 newVoiceChannel = await guild.channels.create({66 name: channelName,67 type: ChannelType.GuildVoice,68 parent: parentId,69 permissionOverwrites: [70 {71 id: member.id,72 allow: [PermissionFlagsBits.Connect, PermissionFlagsBits.ManageChannels, PermissionFlagsBits.MoveMembers],73 },74 {75 id: guild.id, // @everyone76 allow: [PermissionFlagsBits.Connect],77 }78 ],79 });80 81 // Create the Text Channel (Paired)82 let safeTextName = `${member.displayName}-text`.toLowerCase().replace(/[^a-z0-9_-]/g, '').substring(0, 100);83 if (!safeTextName) safeTextName = 'text-channel';84 85 newTextChannel = await guild.channels.create({86 name: safeTextName,87 type: ChannelType.GuildText,88 parent: parentId,89 permissionOverwrites: [90 {91 id: guild.id, // @everyone DENY view92 deny: [PermissionFlagsBits.ViewChannel],93 },94 {95 id: member.id, // Owner ALLOW view96 allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageChannels],97 }98 ],99 });100 101 // Register in DB102 await client.db.addTempChannel(newVoiceChannel.id, member.id, guild.id, newTextChannel.id);103 104 // Move member105 await member.voice.setChannel(newVoiceChannel);106 107 // Send Dashboard Embed108 const embed = new EmbedBuilder()109 .setTitle('Voice Channel Control')110 .setDescription(`Welcome to your private channel, **${member.displayName}**!\nThis text channel is paired with your voice channel.`)111 .setColor(0x00FF00)112 .addFields(113 { name: 'Commands', value: '`/vc name`, `/vc limit`, `/vc lock`', inline: true }114 );115 116 const row = new ActionRowBuilder()117 .addComponents(118 new ButtonBuilder().setCustomId('vc_lock').setLabel('Lock').setEmoji('๐').setStyle(ButtonStyle.Secondary),119 new ButtonBuilder().setCustomId('vc_unlock').setLabel('Unlock').setEmoji('๐').setStyle(ButtonStyle.Secondary),120 new ButtonBuilder().setCustomId('vc_rename_ask').setLabel('Rename').setEmoji('โ๏ธ').setStyle(ButtonStyle.Primary),121 new ButtonBuilder().setCustomId('vc_kick_menu').setLabel('Kick User').setEmoji('๐ซ').setStyle(ButtonStyle.Danger)122 );123 124 await newTextChannel.send({ content: `<@${member.id}>`, embeds: [embed], components: [row] });125 126 } catch (error) {127 console.error('Error creating temp channel:', error);128 129 // ROLLBACK: Cleanup if something went wrong (e.g., user disconnected before move)130 if (newVoiceChannel) {131 try {132 await newVoiceChannel.delete();133 await client.db.removeTempChannel(newVoiceChannel.id);134 } catch (e) {135 console.error("Failed to cleanup voice channel on Discord API. Leaving in DB to avoid ghost channel:", e);136 }137 }138 if (newTextChannel) {139 await newTextChannel.delete().catch(e => console.error("Failed to cleanup text channel:", e));140 }141 }142}143 144async function checkDeleteTempChannel(channel, tempChannelData, client) {145 // Check if empty146 if (channel.members.size === 0) {147 let voiceDeleted = false;148 149 try {150 await channel.delete();151 voiceDeleted = true;152 } catch (error) {153 console.error('Error deleting temp voice channel:', error.message || error);154 }155 156 // Try to delete Paired Text Channel (Cache Resilient)157 if (tempChannelData.text_channel_id) {158 try {159 const textChannel = await channel.guild.channels.fetch(tempChannelData.text_channel_id);160 if (textChannel) {161 await textChannel.delete();162 }163 } catch (textError) {164 if (textError.code !== 10003) { // 10003 is Unknown Channel165 console.error('Error fetching/deleting paired text channel during cleanup:', textError.message || textError);166 }167 }168 }169 170 // Only remove from DB if voice was successfully deleted171 if (voiceDeleted) {172 try {173 await client.db.removeTempChannel(channel.id);174 } catch (dbError) {175 console.error('Error removing channel from DB:', dbError.message || dbError);176 }177 }178 }179}180 