diff --git a/src/commands/join.ts b/src/commands/join.ts index b7951f4..e829467 100644 --- a/src/commands/join.ts +++ b/src/commands/join.ts @@ -2,24 +2,21 @@ import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, - ChatInputCommandInteraction, // Import the specific _interaction type - GuildMember, // Import GuildMember type - VoiceBasedChannel, // Import VoiceBasedChannel type + ChatInputCommandInteraction, + GuildMember, + VoiceBasedChannel, } from "discord.js"; -import logger from "../utils/logger.js"; // Use default import -import { BotClient } from "../index.js"; // Import the BotClient interface -import { Player } from "shoukaku"; // Import the Player type explicitly +import logger from "../utils/logger.js"; +import { BotClient } from "../index.js"; +import { Player } from "shoukaku"; 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(() => {}); @@ -46,9 +43,8 @@ export default { const currentVoiceChannel = voiceChannel as VoiceBasedChannel; // 2. Check bot permissions - const permissions = currentVoiceChannel.permissionsFor(_client.user!); // Use non-null assertion for _client.user + const permissions = currentVoiceChannel.permissionsFor(_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)) { @@ -67,77 +63,65 @@ export default { } // 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) { + // First, ensure clean state by disconnecting if already connected + if (player) { try { - // Create player using the Shoukaku manager + logger.info(`Destroying existing player for guild ${_interaction.guildId} before reconnecting`); + await player.destroy(); + player = undefined; + } catch (error) { + logger.warn(`Error destroying existing player: ${error}`); + // Continue with connection attempt anyway + } + } + + // Attempt to join voice channel with retry logic + let attempts = 0; + const maxAttempts = 3; + + while (attempts < maxAttempts) { + attempts++; + try { + // Wait a short time between retries to allow Discord's voice state to update + if (attempts > 1) { + await new Promise(resolve => setTimeout(resolve, 1000)); + logger.info(`Attempt ${attempts} to join voice channel ${currentVoiceChannel.id}`); + } + player = await shoukaku.joinVoiceChannel({ guildId: _interaction.guildId, channelId: currentVoiceChannel.id, - shardId: _interaction.guild.shardId, // Get shardId from guild + shardId: _interaction.guild.shardId, + deaf: true // Set to true to avoid listening to voice data, saves bandwidth }); logger.info( `Created player and connected to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) in guild ${_interaction.guild.name} (${_interaction.guildId})`, ); + + // Connection was successful await _interaction.editReply(`Joined ${currentVoiceChannel.name}! Ready to play music.`); + return; } 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}`, + `Attempt ${attempts}: Failed to connect to voice channel 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) { + + // Clean up any partial connections on failure 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.", - ); + await shoukaku.leaveVoiceChannel(_interaction.guildId); + } catch (leaveError) { + // Ignore leave errors + } + + if (attempts === maxAttempts) { + return _interaction.editReply(`Failed to join voice channel after ${maxAttempts} attempts. Please try again later.`); } - } 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}`); - // } } }, }; diff --git a/src/commands/play.ts b/src/commands/play.ts index 29b8ae0..8ba3ea6 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -180,34 +180,75 @@ export default { player = shoukaku.players.get(_interaction.guildId) as GuildPlayer | undefined; const connection = shoukaku.connections.get(_interaction.guildId); + // Check if we need to join or move to a different channel if (!player || !connection || connection.channelId !== currentVoiceChannel.id) { - // If player/connection doesn't exist or bot is in wrong channel, join/move - try { - player = (await shoukaku.joinVoiceChannel({ - guildId: _interaction.guildId, - channelId: currentVoiceChannel.id, - shardId: _interaction.guild.shardId, - })) as GuildPlayer; // Cast to extended type - logger.info( - `Joined/Moved to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) for play command.`, - ); - // Initialize queue if it's a new player - if (!player.queue) { - player.queue = []; + // If existing player, destroy it for a clean slate + if (player) { + try { + logger.info(`Destroying existing player for guild ${_interaction.guildId} before reconnecting`); + await player.destroy(); + player = undefined; + } catch (error) { + logger.warn(`Error destroying existing player: ${error}`); + // Continue with connection attempt anyway + } + } + + // Attempt to join voice channel with retry logic + let attempts = 0; + const maxAttempts = 3; + let joinSuccess = false; + + while (attempts < maxAttempts && !joinSuccess) { + attempts++; + try { + // Wait a short time between retries to allow Discord's voice state to update + if (attempts > 1) { + await new Promise(resolve => setTimeout(resolve, 1000)); + logger.info(`Attempt ${attempts} to join voice channel ${currentVoiceChannel.id}`); + } + + player = (await shoukaku.joinVoiceChannel({ + guildId: _interaction.guildId, + channelId: currentVoiceChannel.id, + shardId: _interaction.guild.shardId, + deaf: true // Set to true to avoid listening to voice data, saves bandwidth + })) as GuildPlayer; + + // Initialize queue if it's a new player + if (!player.queue) { + player.queue = []; + } + player.textChannelId = _interaction.channelId; // Store text channel context + + logger.info( + `Joined/Moved to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) for play command.`, + ); + + joinSuccess = true; + } catch (joinError: unknown) { + const errorMsg = joinError instanceof Error ? joinError.message : String(joinError); + logger.error( + `Attempt ${attempts}: Failed to join voice channel for guild ${_interaction.guildId}: ${errorMsg}`, + joinError, + ); + + // Clean up any partial connections on failure + try { + await shoukaku.leaveVoiceChannel(_interaction.guildId); + } catch (leaveError) { + // Ignore leave errors + } + + if (attempts === maxAttempts) { + return _interaction.editReply( + "Failed to join the voice channel after multiple attempts. Please try again later." + ); + } } - player.textChannelId = _interaction.channelId; // Store text channel context - } catch (joinError: unknown) { - const errorMsg = joinError instanceof Error ? joinError.message : String(joinError); - logger.error( - `Failed to join/move player for guild ${_interaction.guildId}: ${errorMsg}`, - joinError, - ); - shoukaku.leaveVoiceChannel(_interaction.guildId).catch(() => {}); // Attempt cleanup - return _interaction.editReply( - "An error occurred while trying to join the voice channel.", - ); } } else { + // We already have a player connected to the right channel // Ensure queue exists if player was retrieved if (!player.queue) { player.queue = []; @@ -271,10 +312,9 @@ export default { responseEmbed .setTitle("Track Added to Queue") .setDescription(`[${track.info.title}](${track.info.uri})`) - // Ensure player exists before accessing queue .addFields({ name: "Position in queue", - value: `${player.queue.length + 1}`, + value: `${player?.queue?.length ?? 0 + 1}`, // Add null checks inline: true, }); if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl); // Use artworkUrl @@ -298,7 +338,7 @@ export default { .setDescription(`[${track.info.title}](${track.info.uri})`) .addFields({ name: "Position in queue", - value: `${player.queue.length + 1}`, + value: `${player?.queue?.length ?? 0 + 1}`, // Add null checks inline: true, }); if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl);