Team Ai
Apppublic

Stryker-SC/Discord-Bot-VCMaster-JavaScript-Revamped

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
voiceStateUpdate.js180 linesDownload Raw Back to events
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