import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ChatInputCommandInteraction, // Import the specific interaction type GuildMember, // Import GuildMember type VoiceBasedChannel // Import VoiceBasedChannel type } from 'discord.js'; import logger from '../utils/logger'; // Use default import import { BotClient } from '../index'; // Import the BotClient interface import { Player } from 'shoukaku'; // Import the Player type explicitly export default { // Use export default for ES Modules data: new SlashCommandBuilder() .setName('join') .setDescription('Joins your current voice channel'), async execute(interaction: ChatInputCommandInteraction, client: BotClient) { // Add types // Ensure command is run in a guild if (!interaction.guildId || !interaction.guild || !interaction.channelId) { // Reply might fail if interaction is already replied/deferred, use editReply if needed return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }).catch(() => {}); } // Ensure interaction.member is a GuildMember if (!(interaction.member instanceof GuildMember)) { return interaction.reply({ content: 'Could not determine your voice channel.', ephemeral: true }).catch(() => {}); } // Use ephemeral deferral await interaction.deferReply({ ephemeral: true }); const member = interaction.member; // Already checked it's GuildMember const voiceChannel = member?.voice?.channel; // 1. Check if user is in a voice channel if (!voiceChannel) { return interaction.editReply('You need to be in a voice channel to use this command!'); } // Type assertion for voiceChannel after check const currentVoiceChannel = voiceChannel as VoiceBasedChannel; // 2. Check bot permissions const permissions = currentVoiceChannel.permissionsFor(client.user!); // Use non-null assertion for client.user if (!permissions?.has(PermissionFlagsBits.Connect)) { // Optional chaining for permissions return interaction.editReply('I need permission to **connect** to your voice channel!'); } if (!permissions?.has(PermissionFlagsBits.Speak)) { return interaction.editReply('I need permission to **speak** in your voice channel!'); } // Ensure it's a voice channel (not stage, etc.) if (currentVoiceChannel.type !== ChannelType.GuildVoice) { return interaction.editReply('I can only join standard voice channels.'); } // Get the initialized Shoukaku instance from the client object const shoukaku = client.shoukaku; if (!shoukaku) { logger.error('Shoukaku instance not found on client object!'); return interaction.editReply('The music player is not ready yet. Please try again shortly.'); } // 3. Get or create the player and connect using Shoukaku // Correctly get player from the players map and type it let player: Player | undefined = shoukaku.players.get(interaction.guildId); if (!player) { try { // Create player using the Shoukaku manager player = await shoukaku.joinVoiceChannel({ guildId: interaction.guildId, channelId: currentVoiceChannel.id, shardId: interaction.guild.shardId, // Get shardId from guild }); logger.info(`Created player and connected to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) in guild ${interaction.guild.name} (${interaction.guildId})`); await interaction.editReply(`Joined ${currentVoiceChannel.name}! Ready to play music.`); } catch (error: unknown) { // Type error as unknown const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to create/connect player for guild ${interaction.guildId}: ${errorMessage}`, error); // Attempt to leave voice channel if connection failed partially shoukaku.leaveVoiceChannel(interaction.guildId).catch((e: unknown) => { // Type catch error const leaveErrorMsg = e instanceof Error ? e.message : String(e); logger.error(`Error leaving VC after failed join: ${leaveErrorMsg}`); }); return interaction.editReply('An error occurred while trying to join the voice channel.'); } } else { // If player exists, get the corresponding connection const connection = shoukaku.connections.get(interaction.guildId); // Check if connection exists and if it's in a different channel if (!connection || connection.channelId !== currentVoiceChannel.id) { try { // Rejoining should handle moving the bot // Note: joinVoiceChannel might implicitly destroy the old player/connection if one exists for the guild. // If issues arise, explicitly call leaveVoiceChannel first. player = await shoukaku.joinVoiceChannel({ guildId: interaction.guildId, channelId: currentVoiceChannel.id, shardId: interaction.guild.shardId, }); logger.info(`Moved player to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) in guild ${interaction.guildId}`); await interaction.editReply(`Moved to ${currentVoiceChannel.name}!`); } catch (error: unknown) { // Type error as unknown const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to move player for guild ${interaction.guildId}: ${errorMessage}`, error); return interaction.editReply('An error occurred while trying to move to the voice channel.'); } } else { // Already in the correct channel await interaction.editReply(`I'm already in ${currentVoiceChannel.name}!`); } // Example of updating a manually managed text channel context (if needed) // if (player.textChannelId !== interaction.channelId) { // player.textChannelId = interaction.channelId; // logger.debug(`Updated player text channel context to ${interaction.channel?.name} (${interaction.channelId}) in guild ${interaction.guildId}`); // } } }, };