Compare commits
5 Commits
72a59bbcdd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a2c9121012 | |||
| 1aa97a8a7a | |||
| c613ef3f35 | |||
| 9fd3f4a678 | |||
| a324815788 |
38
Dockerfile
38
Dockerfile
@@ -3,24 +3,22 @@ FROM node:23-alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install pnpm and necessary build tools (if native modules are used)
|
# Install pnpm and necessary build tools
|
||||||
RUN apk add --no-cache python3 make g++ pnpm
|
RUN apk add --no-cache python3 make g++ pnpm
|
||||||
|
|
||||||
# Copy package manifests
|
# First copy all config files
|
||||||
|
COPY tsconfig.json tsconfig.deploy.json ./
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
# Install ALL dependencies (including devDependencies needed for build)
|
# Now copy source code
|
||||||
|
COPY src/ ./src/
|
||||||
|
COPY deploy-commands.ts ./
|
||||||
|
|
||||||
|
# Install dependencies AFTER copying config files
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Copy the rest of the source code
|
# Build the TypeScript code directly
|
||||||
COPY . .
|
RUN npx tsc -p tsconfig.json && npx tsc -p tsconfig.deploy.json
|
||||||
|
|
||||||
# Compile TypeScript
|
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
# Prune devDependencies after build (optional but good practice)
|
|
||||||
RUN pnpm prune --prod
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Production Stage ----
|
# ---- Production Stage ----
|
||||||
FROM node:23-alpine
|
FROM node:23-alpine
|
||||||
@@ -29,17 +27,17 @@ WORKDIR /app
|
|||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Copy necessary files from the builder stage
|
# Copy application files
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json ./package.json
|
||||||
# Copy other runtime necessities (adjust if needed)
|
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
# COPY .env.example ./
|
COPY application.yml ./application.yml
|
||||||
# COPY application.yml ./
|
COPY plugins ./plugins
|
||||||
# COPY plugins ./plugins
|
|
||||||
|
|
||||||
# Expose port if needed (though likely not for a Discord bot)
|
# Install production dependencies only
|
||||||
# EXPOSE 3000
|
# Temporarily disable the prepare script by setting npm_config_ignore_scripts
|
||||||
|
RUN apk add --no-cache pnpm && \
|
||||||
|
npm_config_ignore_scripts=true pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
# Run the compiled JavaScript application
|
# Run the compiled JavaScript application
|
||||||
CMD ["node", "dist/index.js"]
|
CMD ["node", "dist/index.js"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { REST, Routes, APIApplicationCommand } from "discord.js";
|
import { REST, Routes, APIApplicationCommand } from "discord.js";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import logger from "./src/utils/logger"; // Use default import now
|
import logger from "./src/utils/logger.js"; // Added .js extension for ES modules
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
@@ -33,8 +33,9 @@ const loadCommandsForDeployment = async () => {
|
|||||||
for (const file of commandFiles) {
|
for (const file of commandFiles) {
|
||||||
const filePath = path.join(commandsPath, file);
|
const filePath = path.join(commandsPath, file);
|
||||||
try {
|
try {
|
||||||
// Use dynamic import
|
// Use dynamic import with file:// protocol for ES modules
|
||||||
const commandModule = await import(filePath);
|
const fileUrl = new URL(`file://${filePath}`);
|
||||||
|
const commandModule = await import(fileUrl.href);
|
||||||
// Assuming commands export default or have a 'default' property
|
// Assuming commands export default or have a 'default' property
|
||||||
const command = commandModule.default || commandModule;
|
const command = commandModule.default || commandModule;
|
||||||
|
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "discord-music-bot",
|
"name": "discord-music-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"build:deploy": "tsc -p tsconfig.deploy.json",
|
||||||
|
"build:all": "npm run build && npm run build:deploy",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write src/**/*.ts deploy-commands.ts",
|
"format": "prettier --write src/**/*.ts deploy-commands.ts",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build:all"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
60
scripts/fix-imports.cjs
Executable file
60
scripts/fix-imports.cjs
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Get all TypeScript files in a directory recursively
|
||||||
|
function getTypeScriptFiles(dir) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
function traverse(currentDir) {
|
||||||
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
traverse(fullPath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(dir);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix imports in a file
|
||||||
|
function fixImportsInFile(filePath) {
|
||||||
|
console.log(`Processing ${filePath}`);
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Regular expression to match relative imports without file extensions
|
||||||
|
const importRegex = /(import\s+(?:[^'"]*\s+from\s+)?['"])(\.\.[^'"]*?)(['"])/g;
|
||||||
|
|
||||||
|
// Add .js extension to relative imports
|
||||||
|
content = content.replace(importRegex, (match, start, importPath, end) => {
|
||||||
|
// Don't add extension if it already has one or ends with a directory
|
||||||
|
if (importPath.endsWith('.js') || importPath.endsWith('/')) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return `${start}${importPath}.js${end}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
function main() {
|
||||||
|
const srcDir = path.join(__dirname, '..', 'src');
|
||||||
|
const files = getTypeScriptFiles(srcDir);
|
||||||
|
|
||||||
|
console.log(`Found ${files.length} TypeScript files`);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
fixImportsInFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
60
scripts/fix-imports.js
Executable file
60
scripts/fix-imports.js
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Get all TypeScript files in a directory recursively
|
||||||
|
function getTypeScriptFiles(dir) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
function traverse(currentDir) {
|
||||||
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
traverse(fullPath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(dir);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix imports in a file
|
||||||
|
function fixImportsInFile(filePath) {
|
||||||
|
console.log(`Processing ${filePath}`);
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Regular expression to match relative imports without file extensions
|
||||||
|
const importRegex = /(import\s+(?:[^'"]*\s+from\s+)?['"])(\.\.[^'"]*?)(['"])/g;
|
||||||
|
|
||||||
|
// Add .js extension to relative imports
|
||||||
|
content = content.replace(importRegex, (match, start, importPath, end) => {
|
||||||
|
// Don't add extension if it already has one or ends with a directory
|
||||||
|
if (importPath.endsWith('.js') || importPath.endsWith('/')) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return `${start}${importPath}.js${end}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
function main() {
|
||||||
|
const srcDir = path.join(__dirname, '..', 'src');
|
||||||
|
const files = getTypeScriptFiles(srcDir);
|
||||||
|
|
||||||
|
console.log(`Found ${files.length} TypeScript files`);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
fixImportsInFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -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"; // Use default import
|
import logger from "../utils/logger.js";
|
||||||
import { BotClient } from "../index"; // 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,
|
|
||||||
shardId: _interaction.guild.shardId,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(
|
if (attempts === maxAttempts) {
|
||||||
`Moved player to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) in guild ${_interaction.guildId}`,
|
return _interaction.editReply(`Failed to join voice channel after ${maxAttempts} attempts. Please try again later.`);
|
||||||
);
|
|
||||||
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}`);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
ChatInputCommandInteraction, // Import the specific _interaction type
|
ChatInputCommandInteraction, // Import the specific _interaction type
|
||||||
GuildMember, // Import GuildMember type
|
GuildMember, // Import GuildMember type
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import logger from "../utils/logger"; // Use default import
|
import logger from "../utils/logger.js"; // Use default import
|
||||||
import { BotClient } from "../index"; // Import the BotClient interface
|
import { BotClient } from "../index.js"; // Import the BotClient interface
|
||||||
// No need to import Player explicitly if we just check connection
|
// No need to import Player explicitly if we just check connection
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
GuildMember,
|
GuildMember,
|
||||||
VoiceBasedChannel,
|
VoiceBasedChannel,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger.js";
|
||||||
import { BotClient } from "../index";
|
import { BotClient } from "../index.js";
|
||||||
// Import necessary Shoukaku types - LavalinkResponse might need a local definition if not exported
|
// Import necessary Shoukaku types - LavalinkResponse might need a local definition if not exported
|
||||||
import { Player, Node, Track, SearchResult, Connection } from "shoukaku";
|
import { Player, Node, Track, SearchResult, Connection } from "shoukaku";
|
||||||
|
|
||||||
@@ -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
|
||||||
|
if (player) {
|
||||||
try {
|
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({
|
player = (await shoukaku.joinVoiceChannel({
|
||||||
guildId: _interaction.guildId,
|
guildId: _interaction.guildId,
|
||||||
channelId: currentVoiceChannel.id,
|
channelId: currentVoiceChannel.id,
|
||||||
shardId: _interaction.guild.shardId,
|
shardId: _interaction.guild.shardId,
|
||||||
})) as GuildPlayer; // Cast to extended type
|
deaf: true // Set to true to avoid listening to voice data, saves bandwidth
|
||||||
logger.info(
|
})) as GuildPlayer;
|
||||||
`Joined/Moved to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) for play command.`,
|
|
||||||
);
|
|
||||||
// Initialize queue if it's a new player
|
// Initialize queue if it's a new player
|
||||||
if (!player.queue) {
|
if (!player.queue) {
|
||||||
player.queue = [];
|
player.queue = [];
|
||||||
}
|
}
|
||||||
player.textChannelId = _interaction.channelId; // Store text channel context
|
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) {
|
} catch (joinError: unknown) {
|
||||||
const errorMsg = joinError instanceof Error ? joinError.message : String(joinError);
|
const errorMsg = joinError instanceof Error ? joinError.message : String(joinError);
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to join/move player for guild ${_interaction.guildId}: ${errorMsg}`,
|
`Attempt ${attempts}: Failed to join voice channel for guild ${_interaction.guildId}: ${errorMsg}`,
|
||||||
joinError,
|
joinError,
|
||||||
);
|
);
|
||||||
shoukaku.leaveVoiceChannel(_interaction.guildId).catch(() => {}); // Attempt cleanup
|
|
||||||
|
// Clean up any partial connections on failure
|
||||||
|
try {
|
||||||
|
await shoukaku.leaveVoiceChannel(_interaction.guildId);
|
||||||
|
} catch (leaveError) {
|
||||||
|
// Ignore leave errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts === maxAttempts) {
|
||||||
return _interaction.editReply(
|
return _interaction.editReply(
|
||||||
"An error occurred while trying to join the voice channel.",
|
"Failed to join the voice channel after multiple attempts. Please try again later."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} 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);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Events, Interaction } from "discord.js";
|
import { Events, Interaction } from "discord.js";
|
||||||
import { BotClient } from "../types/botClient";
|
import { BotClient } from "../types/botClient.js";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.InteractionCreate,
|
name: Events.InteractionCreate,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Events, ActivityType, Client } from "discord.js"; // Import base Client type
|
import { Events, ActivityType, Client } from "discord.js"; // Import base Client type
|
||||||
import logger from "../utils/logger"; // Use default import
|
import logger from "../utils/logger.js"; // Use default import
|
||||||
import { initializeShoukaku } from "../structures/ShoukakuEvents"; // Import the correct setup function
|
import { initializeShoukaku } from "../structures/ShoukakuEvents.js"; // Import the correct setup function
|
||||||
import { BotClient } from "../index"; // Import BotClient type
|
import { BotClient } from "../index.js"; // Import BotClient type
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Use export default
|
// Use export default
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Events, VoiceState, ChannelType } from "discord.js"; // Added ChannelType
|
import { Events, VoiceState, ChannelType } from "discord.js"; // Added ChannelType
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger.js";
|
||||||
import { BotClient } from "../index"; // Assuming BotClient is exported from index
|
import { BotClient } from "../index.js"; // Assuming BotClient is exported from index
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Use export default for ES modules
|
// Use export default for ES modules
|
||||||
|
|||||||
80
src/index.ts
80
src/index.ts
@@ -4,38 +4,40 @@ import {
|
|||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
Collection,
|
Collection,
|
||||||
Events,
|
Events,
|
||||||
BaseInteraction, // Use a base type for now, refine later if needed
|
BaseInteraction,
|
||||||
SlashCommandBuilder, // Assuming commands use this
|
SlashCommandBuilder,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { Shoukaku, Connectors, NodeOption, ShoukakuOptions } from "shoukaku";
|
import { Shoukaku, Connectors, NodeOption, ShoukakuOptions } from "shoukaku";
|
||||||
import logger from "./utils/logger"; // Assuming logger uses export default or similar
|
import logger from "./utils/logger.js"; // Add .js extension
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
// import { fileURLToPath } from 'url'; // Needed for __dirname in ES Modules if module is not CommonJS
|
import { fileURLToPath } from 'url'; // Needed for __dirname in ES Modules
|
||||||
|
|
||||||
|
// Get __dirname equivalent in ES Modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
// Define Command structure
|
// Define Command structure
|
||||||
interface Command {
|
interface Command {
|
||||||
data: Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">; // Or appropriate type for your command data
|
data: Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
|
||||||
execute: (_interaction: BaseInteraction, _client: BotClient) => Promise<void>; // Adjust _interaction type if needed
|
execute: (_interaction: BaseInteraction, _client: BotClient) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define Event structure
|
// Define Event structure
|
||||||
interface BotEvent {
|
interface BotEvent {
|
||||||
name: string; // Should match discord.js event names or custom names
|
name: string;
|
||||||
once?: boolean;
|
once?: boolean;
|
||||||
execute: (..._args: any[]) => void; // Use specific types later if possible
|
execute: (..._args: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extend the discord.js Client class to include custom properties
|
// Extend the discord.js Client class to include custom properties
|
||||||
export interface BotClient extends Client {
|
export interface BotClient extends Client {
|
||||||
// Add export keyword
|
|
||||||
commands: Collection<string, Command>;
|
commands: Collection<string, Command>;
|
||||||
shoukaku: Shoukaku;
|
shoukaku: Shoukaku;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
// __dirname is available in CommonJS modules, which is set in tsconfig.json
|
|
||||||
|
|
||||||
// Validate essential environment variables
|
// Validate essential environment variables
|
||||||
if (!process.env.DISCORD_TOKEN) {
|
if (!process.env.DISCORD_TOKEN) {
|
||||||
@@ -46,8 +48,6 @@ if (!process.env.LAVALINK_HOST || !process.env.LAVALINK_PORT || !process.env.LAV
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
"Lavalink connection details (HOST, PORT, PASSWORD) are missing or incomplete in .env. Music functionality will be limited.",
|
"Lavalink connection details (HOST, PORT, PASSWORD) are missing or incomplete in .env. Music functionality will be limited.",
|
||||||
);
|
);
|
||||||
// Decide if the bot should exit or continue without music
|
|
||||||
// process.exit(1); // Uncomment to exit if Lavalink is mandatory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Discord _client instance with necessary intents
|
// Create a new Discord _client instance with necessary intents
|
||||||
@@ -55,28 +55,27 @@ const _client = new Client({
|
|||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
GatewayIntentBits.GuildMessages, // Add if needed for prefix commands or message content
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.MessageContent, // Add if needed for message content
|
GatewayIntentBits.MessageContent,
|
||||||
],
|
],
|
||||||
}) as BotClient; // Assert the type here
|
}) as BotClient;
|
||||||
|
|
||||||
// Define Shoukaku nodes
|
// Define Shoukaku nodes
|
||||||
const Nodes: NodeOption[] = [
|
const Nodes: NodeOption[] = [
|
||||||
{
|
{
|
||||||
name: process.env.LAVALINK_NAME || "lavalink-node-1", // Use an env var or default name
|
name: process.env.LAVALINK_NAME || "lavalink-node-1",
|
||||||
url: `${process.env.LAVALINK_HOST || "localhost"}:${process.env.LAVALINK_PORT || 2333}`, // Use || 2333 for default port number
|
url: `${process.env.LAVALINK_HOST || "localhost"}:${process.env.LAVALINK_PORT || 2333}`,
|
||||||
auth: process.env.LAVALINK_PASSWORD || "youshallnotpass", // Password from your Lavalink server config
|
auth: process.env.LAVALINK_PASSWORD || "youshallnotpass",
|
||||||
secure: process.env.LAVALINK_SECURE === "true", // Set to true if using HTTPS/WSS
|
secure: process.env.LAVALINK_SECURE === "true",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Shoukaku _options
|
// Shoukaku _options
|
||||||
const shoukakuOptions: ShoukakuOptions = {
|
const shoukakuOptions: ShoukakuOptions = {
|
||||||
moveOnDisconnect: false, // Whether to move players to another node when a node disconnects
|
moveOnDisconnect: false,
|
||||||
resume: true, // Whether to resume players session after Lavalink restarts
|
resume: true,
|
||||||
reconnectTries: 3, // Number of attempts to reconnect to Lavalink
|
reconnectTries: 3,
|
||||||
reconnectInterval: 5000, // Interval between reconnect attempts in milliseconds
|
reconnectInterval: 5000,
|
||||||
// Add other _options as needed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize Shoukaku
|
// Initialize Shoukaku
|
||||||
@@ -84,7 +83,7 @@ _client.shoukaku = new Shoukaku(new Connectors.DiscordJS(_client), Nodes, shouka
|
|||||||
|
|
||||||
// Show the actual Lavalink connection details (without exposing the actual password)
|
// Show the actual Lavalink connection details (without exposing the actual password)
|
||||||
logger.info(
|
logger.info(
|
||||||
`Lavalink connection configured to: ${process.env.LAVALINK_HOST}:${process.env.LAVALINK_PORT} (Password: ${process.env.LAVALINK_PASSWORD ? "[SET]" : "[NOT SET]"})`,
|
`Lavalink connection configured to: ${process.env.LAVALINK_HOST || "localhost"}:${process.env.LAVALINK_PORT || 2333} (Password: ${process.env.LAVALINK_PASSWORD ? "[SET]" : "[NOT SET]"})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Collections for commands
|
// Collections for commands
|
||||||
@@ -92,16 +91,17 @@ _client.commands = new Collection<string, Command>();
|
|||||||
|
|
||||||
// --- Command Loading ---
|
// --- Command Loading ---
|
||||||
const commandsPath = path.join(__dirname, "commands");
|
const commandsPath = path.join(__dirname, "commands");
|
||||||
// Read .ts files now
|
// Read .js files instead of .ts after compilation
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith(".ts"));
|
const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith(".js"));
|
||||||
|
|
||||||
const loadCommands = async () => {
|
const loadCommands = async () => {
|
||||||
for (const file of commandFiles) {
|
for (const file of commandFiles) {
|
||||||
const filePath = path.join(commandsPath, file);
|
const filePath = path.join(commandsPath, file);
|
||||||
try {
|
try {
|
||||||
// Use dynamic import for ES Modules/CommonJS interop
|
// Use dynamic import with file:// protocol for ES Modules
|
||||||
const commandModule = await import(filePath);
|
const fileUrl = new URL(`file://${filePath}`).href;
|
||||||
const command: Command = commandModule.default || commandModule; // Handle default exports
|
const commandModule = await import(fileUrl);
|
||||||
|
const command: Command = commandModule.default || commandModule;
|
||||||
|
|
||||||
if (command && typeof command === "object" && "data" in command && "execute" in command) {
|
if (command && typeof command === "object" && "data" in command && "execute" in command) {
|
||||||
_client.commands.set(command.data.name, command);
|
_client.commands.set(command.data.name, command);
|
||||||
@@ -112,7 +112,6 @@ const loadCommands = async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Type the error as unknown
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(`Error loading command at ${filePath}: ${errorMessage}`, error);
|
logger.error(`Error loading command at ${filePath}: ${errorMessage}`, error);
|
||||||
}
|
}
|
||||||
@@ -121,22 +120,24 @@ const loadCommands = async () => {
|
|||||||
|
|
||||||
// --- Event Handling ---
|
// --- Event Handling ---
|
||||||
const eventsPath = path.join(__dirname, "events");
|
const eventsPath = path.join(__dirname, "events");
|
||||||
// Read .ts files now
|
// Read .js files instead of .ts after compilation
|
||||||
const eventFiles = fs.readdirSync(eventsPath).filter((file: string) => file.endsWith(".ts"));
|
const eventFiles = fs.readdirSync(eventsPath).filter((file: string) => file.endsWith(".js"));
|
||||||
|
|
||||||
const loadEvents = async () => {
|
const loadEvents = async () => {
|
||||||
for (const file of eventFiles) {
|
for (const file of eventFiles) {
|
||||||
const filePath = path.join(eventsPath, file);
|
const filePath = path.join(eventsPath, file);
|
||||||
try {
|
try {
|
||||||
const eventModule = await import(filePath);
|
// Use dynamic import with file:// protocol for ES Modules
|
||||||
const event: BotEvent = eventModule.default || eventModule; // Handle default exports
|
const fileUrl = new URL(`file://${filePath}`).href;
|
||||||
|
const eventModule = await import(fileUrl);
|
||||||
|
const event: BotEvent = eventModule.default || eventModule;
|
||||||
|
|
||||||
if (event && typeof event === "object" && "name" in event && "execute" in event) {
|
if (event && typeof event === "object" && "name" in event && "execute" in event) {
|
||||||
if (event.once) {
|
if (event.once) {
|
||||||
_client.once(event.name, (..._args: any[]) => event.execute(..._args, _client)); // Pass _client
|
_client.once(event.name, (..._args: any[]) => event.execute(..._args, _client));
|
||||||
logger.info(`Loaded event ${event.name} (once)`);
|
logger.info(`Loaded event ${event.name} (once)`);
|
||||||
} else {
|
} else {
|
||||||
_client.on(event.name, (..._args: any[]) => event.execute(..._args, _client)); // Pass _client
|
_client.on(event.name, (..._args: any[]) => event.execute(..._args, _client));
|
||||||
logger.info(`Loaded event ${event.name}`);
|
logger.info(`Loaded event ${event.name}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +162,6 @@ _client.shoukaku.on("error", (name: string, error: Error) =>
|
|||||||
_client.shoukaku.on("close", (name: string, code: number, reason: string | undefined) =>
|
_client.shoukaku.on("close", (name: string, code: number, reason: string | undefined) =>
|
||||||
logger.warn(`Lavalink Node: ${name} closed with code ${code}. Reason: ${reason || "No reason"}`),
|
logger.warn(`Lavalink Node: ${name} closed with code ${code}. Reason: ${reason || "No reason"}`),
|
||||||
);
|
);
|
||||||
// Corrected disconnect event signature based on common usage and error TS148
|
|
||||||
_client.shoukaku.on("disconnect", (name: string, count: number) => {
|
_client.shoukaku.on("disconnect", (name: string, count: number) => {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Lavalink Node: ${name} disconnected. ${count} players were disconnected from this node.`,
|
`Lavalink Node: ${name} disconnected. ${count} players were disconnected from this node.`,
|
||||||
@@ -180,7 +180,7 @@ async function main() {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(`Failed to log in: ${errorMessage}`);
|
logger.error(`Failed to log in: ${errorMessage}`);
|
||||||
process.exit(1); // Exit if login fails
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +197,4 @@ process.on("unhandledRejection", (reason: unknown, promise: Promise<any>) => {
|
|||||||
});
|
});
|
||||||
process.on("uncaughtException", (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => {
|
process.on("uncaughtException", (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => {
|
||||||
logger.error(`Uncaught exception: ${error.message}`, { error, origin });
|
logger.error(`Uncaught exception: ${error.message}`, { error, origin });
|
||||||
// Optional: exit process on critical uncaught exceptions
|
|
||||||
// process.exit(1);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { Shoukaku, NodeOption, ShoukakuOptions, Player, Connectors } from "shoukaku"; // Removed player event types, Added Connectors
|
import { Shoukaku, NodeOption, ShoukakuOptions, Player, Connectors } from 'shoukaku';
|
||||||
// import { Connectors } from 'shoukaku-discord.js'; // Use the discord.js connector - Removed this line
|
import logger from '../utils/logger.js';
|
||||||
import logger from "../utils/logger";
|
import { BotClient } from '../index.js';
|
||||||
import { BotClient } from "../index";
|
|
||||||
// Removed imports from play.ts for now as player listeners are removed
|
// Removed imports from play.ts for now as player listeners are removed
|
||||||
|
|
||||||
// Define Node _options (replace with your actual Lavalink details from .env)
|
// Define Node options (replace with your actual Lavalink details from .env)
|
||||||
const nodes: NodeOption[] = [
|
const nodes: NodeOption[] = [
|
||||||
{
|
{
|
||||||
name: process.env.LAVALINK_NAME || "Lavalink-Node-1",
|
name: process.env.LAVALINK_NAME || 'Lavalink-Node-1',
|
||||||
url: process.env.LAVALINK_URL || "lavalink:2333", // Use service name for Docker Compose if applicable
|
url: process.env.LAVALINK_URL || 'lavalink:2333', // Use service name for Docker Compose if applicable
|
||||||
auth: process.env.LAVALINK_AUTH || "youshallnotpass",
|
auth: process.env.LAVALINK_AUTH || 'youshallnotpass',
|
||||||
secure: process.env.LAVALINK_SECURE === "true" || false,
|
secure: process.env.LAVALINK_SECURE === 'true' || false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define Shoukaku _options
|
// Define Shoukaku options
|
||||||
const shoukakuOptions: ShoukakuOptions = {
|
const shoukakuOptions: ShoukakuOptions = {
|
||||||
moveOnDisconnect: false,
|
moveOnDisconnect: false,
|
||||||
resume: false, // Resume doesn't work reliably across restarts/disconnects without session persistence
|
resume: false, // Resume doesn't work reliably across restarts/disconnects without session persistence
|
||||||
@@ -25,36 +24,36 @@ const shoukakuOptions: ShoukakuOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Function to initialize Shoukaku and attach listeners
|
// Function to initialize Shoukaku and attach listeners
|
||||||
export function initializeShoukaku(_client: BotClient): Shoukaku {
|
export function initializeShoukaku(client: BotClient): Shoukaku {
|
||||||
if (!_client) {
|
if (!client) {
|
||||||
throw new Error("initializeShoukaku requires a _client instance.");
|
throw new Error("initializeShoukaku requires a client instance.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const shoukaku = new Shoukaku(new Connectors.DiscordJS(_client), nodes, shoukakuOptions);
|
const shoukaku = new Shoukaku(new Connectors.DiscordJS(client), nodes, shoukakuOptions);
|
||||||
|
|
||||||
// --- Shoukaku Node Event Listeners ---
|
// --- Shoukaku Node Event Listeners ---
|
||||||
shoukaku.on("ready", (name, resumed) =>
|
shoukaku.on('ready', (name, resumed) =>
|
||||||
logger.info(`Lavalink Node '${name}' ready. Resumed: ${resumed}`),
|
logger.info(`Lavalink Node '${name}' ready. Resumed: ${resumed}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
shoukaku.on("error", (name, error) =>
|
shoukaku.on('error', (name, error) =>
|
||||||
logger.error(`Lavalink Node '${name}' error: ${error.message}`, error),
|
logger.error(`Lavalink Node '${name}' error: ${error.message}`, error)
|
||||||
);
|
);
|
||||||
|
|
||||||
shoukaku.on("close", (name, code, reason) =>
|
shoukaku.on('close', (name, code, reason) =>
|
||||||
logger.warn(`Lavalink Node '${name}' closed. Code: ${code}. Reason: ${reason || "No reason"}`),
|
logger.warn(`Lavalink Node '${name}' closed. Code: ${code}. Reason: ${reason || 'No reason'}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fix: Correct disconnect listener signature
|
// Fix: Correct disconnect listener signature
|
||||||
shoukaku.on("disconnect", (name, count) => {
|
shoukaku.on('disconnect', (name, count) => {
|
||||||
// count = count of players disconnected from the node
|
// count = count of players disconnected from the node
|
||||||
logger.warn(`Lavalink Node '${name}' disconnected. ${count} players disconnected.`);
|
logger.warn(`Lavalink Node '${name}' disconnected. ${count} players disconnected.`);
|
||||||
// If players were not moved, you might want to attempt to reconnect them or clean them up.
|
// If players were not moved, you might want to attempt to reconnect them or clean them up.
|
||||||
});
|
});
|
||||||
|
|
||||||
shoukaku.on("debug", (name, info) => {
|
shoukaku.on('debug', (name, info) => {
|
||||||
// Only log debug messages if not in production or if explicitly enabled
|
// Only log debug messages if not in production or if explicitly enabled
|
||||||
if (process.env.NODE_ENV !== "production" || process.env.LAVALINK_DEBUG === "true") {
|
if (process.env.NODE_ENV !== 'production' || process.env.LAVALINK_DEBUG === 'true') {
|
||||||
logger.debug(`Lavalink Node '${name}' debug: ${info}`);
|
logger.debug(`Lavalink Node '${name}' debug: ${info}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
9
tsconfig.deploy.json
Normal file
9
tsconfig.deploy.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext"
|
||||||
|
},
|
||||||
|
"include": ["deploy-commands.ts"]
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "commonjs",
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
"lib": ["ES2020"],
|
"lib": ["ES2020"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": ".",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -12,11 +13,11 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"moduleResolution": "node"
|
"sourceMap": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*"
|
||||||
"deploy-commands.ts"
|
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
Reference in New Issue
Block a user