refactor: Enhance voice channel joining logic with retry mechanism and cleanup for existing players
This commit is contained in:
parent
1aa97a8a7a
commit
a2c9121012
@ -2,24 +2,21 @@ import {
|
|||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
PermissionFlagsBits,
|
PermissionFlagsBits,
|
||||||
ChannelType,
|
ChannelType,
|
||||||
ChatInputCommandInteraction, // Import the specific _interaction type
|
ChatInputCommandInteraction,
|
||||||
GuildMember, // Import GuildMember type
|
GuildMember,
|
||||||
VoiceBasedChannel, // Import VoiceBasedChannel type
|
VoiceBasedChannel,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import logger from "../utils/logger.js"; // Use default import
|
import logger from "../utils/logger.js";
|
||||||
import { BotClient } from "../index.js"; // Import the BotClient interface
|
import { BotClient } from "../index.js";
|
||||||
import { Player } from "shoukaku"; // Import the Player type explicitly
|
import { Player } from "shoukaku";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Use export default for ES Modules
|
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("join")
|
.setName("join")
|
||||||
.setDescription("Joins your current voice channel"),
|
.setDescription("Joins your current voice channel"),
|
||||||
async execute(_interaction: ChatInputCommandInteraction, _client: BotClient) {
|
async execute(_interaction: ChatInputCommandInteraction, _client: BotClient) {
|
||||||
// Add types
|
|
||||||
// Ensure command is run in a guild
|
// Ensure command is run in a guild
|
||||||
if (!_interaction.guildId || !_interaction.guild || !_interaction.channelId) {
|
if (!_interaction.guildId || !_interaction.guild || !_interaction.channelId) {
|
||||||
// Reply might fail if _interaction is already replied/deferred, use editReply if needed
|
|
||||||
return _interaction
|
return _interaction
|
||||||
.reply({ content: "This command can only be used in a server.", ephemeral: true })
|
.reply({ content: "This command can only be used in a server.", ephemeral: true })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@ -46,9 +43,8 @@ export default {
|
|||||||
const currentVoiceChannel = voiceChannel as VoiceBasedChannel;
|
const currentVoiceChannel = voiceChannel as VoiceBasedChannel;
|
||||||
|
|
||||||
// 2. Check bot permissions
|
// 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)) {
|
if (!permissions?.has(PermissionFlagsBits.Connect)) {
|
||||||
// Optional chaining for permissions
|
|
||||||
return _interaction.editReply("I need permission to **connect** to your voice channel!");
|
return _interaction.editReply("I need permission to **connect** to your voice channel!");
|
||||||
}
|
}
|
||||||
if (!permissions?.has(PermissionFlagsBits.Speak)) {
|
if (!permissions?.has(PermissionFlagsBits.Speak)) {
|
||||||
@ -67,77 +63,65 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get or create the player and connect using Shoukaku
|
// 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);
|
let player: Player | undefined = shoukaku.players.get(_interaction.guildId);
|
||||||
|
|
||||||
if (!player) {
|
// First, ensure clean state by disconnecting if already connected
|
||||||
|
if (player) {
|
||||||
try {
|
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({
|
player = await shoukaku.joinVoiceChannel({
|
||||||
guildId: _interaction.guildId,
|
guildId: _interaction.guildId,
|
||||||
channelId: currentVoiceChannel.id,
|
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(
|
logger.info(
|
||||||
`Created player and connected to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) in guild ${_interaction.guild.name} (${_interaction.guildId})`,
|
`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.`);
|
await _interaction.editReply(`Joined ${currentVoiceChannel.name}! Ready to play music.`);
|
||||||
|
return;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Type error as unknown
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
logger.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,
|
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
|
// Clean up any partial connections on failure
|
||||||
if (!connection || connection.channelId !== currentVoiceChannel.id) {
|
|
||||||
try {
|
try {
|
||||||
// Rejoining should handle moving the bot
|
await shoukaku.leaveVoiceChannel(_interaction.guildId);
|
||||||
// Note: joinVoiceChannel might implicitly destroy the old player/connection if one exists for the guild.
|
} catch (leaveError) {
|
||||||
// If issues arise, explicitly call leaveVoiceChannel first.
|
// Ignore leave errors
|
||||||
player = await shoukaku.joinVoiceChannel({
|
}
|
||||||
guildId: _interaction.guildId,
|
|
||||||
channelId: currentVoiceChannel.id,
|
if (attempts === maxAttempts) {
|
||||||
shardId: _interaction.guild.shardId,
|
return _interaction.editReply(`Failed to join voice channel after ${maxAttempts} attempts. Please try again later.`);
|
||||||
});
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -180,34 +180,75 @@ export default {
|
|||||||
player = shoukaku.players.get(_interaction.guildId) as GuildPlayer | undefined;
|
player = shoukaku.players.get(_interaction.guildId) as GuildPlayer | undefined;
|
||||||
const connection = shoukaku.connections.get(_interaction.guildId);
|
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 || connection.channelId !== currentVoiceChannel.id) {
|
||||||
// If player/connection doesn't exist or bot is in wrong channel, join/move
|
// If existing player, destroy it for a clean slate
|
||||||
try {
|
if (player) {
|
||||||
player = (await shoukaku.joinVoiceChannel({
|
try {
|
||||||
guildId: _interaction.guildId,
|
logger.info(`Destroying existing player for guild ${_interaction.guildId} before reconnecting`);
|
||||||
channelId: currentVoiceChannel.id,
|
await player.destroy();
|
||||||
shardId: _interaction.guild.shardId,
|
player = undefined;
|
||||||
})) as GuildPlayer; // Cast to extended type
|
} catch (error) {
|
||||||
logger.info(
|
logger.warn(`Error destroying existing player: ${error}`);
|
||||||
`Joined/Moved to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) for play command.`,
|
// Continue with connection attempt anyway
|
||||||
);
|
}
|
||||||
// Initialize queue if it's a new player
|
}
|
||||||
if (!player.queue) {
|
|
||||||
player.queue = [];
|
// 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 {
|
} else {
|
||||||
|
// We already have a player connected to the right channel
|
||||||
// Ensure queue exists if player was retrieved
|
// Ensure queue exists if player was retrieved
|
||||||
if (!player.queue) {
|
if (!player.queue) {
|
||||||
player.queue = [];
|
player.queue = [];
|
||||||
@ -271,10 +312,9 @@ export default {
|
|||||||
responseEmbed
|
responseEmbed
|
||||||
.setTitle("Track Added to Queue")
|
.setTitle("Track Added to Queue")
|
||||||
.setDescription(`[${track.info.title}](${track.info.uri})`)
|
.setDescription(`[${track.info.title}](${track.info.uri})`)
|
||||||
// Ensure player exists before accessing queue
|
|
||||||
.addFields({
|
.addFields({
|
||||||
name: "Position in queue",
|
name: "Position in queue",
|
||||||
value: `${player.queue.length + 1}`,
|
value: `${player?.queue?.length ?? 0 + 1}`, // Add null checks
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl); // Use artworkUrl
|
if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl); // Use artworkUrl
|
||||||
@ -298,7 +338,7 @@ export default {
|
|||||||
.setDescription(`[${track.info.title}](${track.info.uri})`)
|
.setDescription(`[${track.info.title}](${track.info.uri})`)
|
||||||
.addFields({
|
.addFields({
|
||||||
name: "Position in queue",
|
name: "Position in queue",
|
||||||
value: `${player.queue.length + 1}`,
|
value: `${player?.queue?.length ?? 0 + 1}`, // Add null checks
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl);
|
if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user