- Converted all .js files to .ts - Added TypeScript configuration (tsconfig.json) - Added ESLint and Prettier configuration - Updated package.json dependencies - Modified Docker and application configurations
187 lines
8.2 KiB
TypeScript
187 lines
8.2 KiB
TypeScript
import dotenv from 'dotenv';
|
|
import {
|
|
Client,
|
|
GatewayIntentBits,
|
|
Collection,
|
|
Events,
|
|
BaseInteraction, // Use a base type for now, refine later if needed
|
|
SlashCommandBuilder, // Assuming commands use this
|
|
} from 'discord.js';
|
|
import { Shoukaku, Connectors, NodeOption, ShoukakuOptions } from 'shoukaku';
|
|
import logger from './utils/logger'; // Assuming logger uses export default or similar
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
// import { fileURLToPath } from 'url'; // Needed for __dirname in ES Modules if module is not CommonJS
|
|
|
|
// Define Command structure
|
|
interface Command {
|
|
data: Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">; // Or appropriate type for your command data
|
|
execute: (interaction: BaseInteraction, client: BotClient) => Promise<void>; // Adjust interaction type if needed
|
|
}
|
|
|
|
// Define Event structure
|
|
interface BotEvent {
|
|
name: string; // Should match discord.js event names or custom names
|
|
once?: boolean;
|
|
execute: (...args: any[]) => void; // Use specific types later if possible
|
|
}
|
|
|
|
// Extend the discord.js Client class to include custom properties
|
|
export interface BotClient extends Client { // Add export keyword
|
|
commands: Collection<string, Command>;
|
|
shoukaku: Shoukaku;
|
|
}
|
|
|
|
// --- Setup ---
|
|
dotenv.config();
|
|
// __dirname is available in CommonJS modules, which is set in tsconfig.json
|
|
|
|
// Validate essential environment variables
|
|
if (!process.env.DISCORD_TOKEN) {
|
|
logger.error('DISCORD_TOKEN is missing in the .env file!');
|
|
process.exit(1);
|
|
}
|
|
if (!process.env.LAVALINK_HOST || !process.env.LAVALINK_PORT || !process.env.LAVALINK_PASSWORD) {
|
|
logger.warn('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
|
|
const client = new Client({
|
|
intents: [
|
|
GatewayIntentBits.Guilds,
|
|
GatewayIntentBits.GuildVoiceStates,
|
|
GatewayIntentBits.GuildMessages, // Add if needed for prefix commands or message content
|
|
GatewayIntentBits.MessageContent, // Add if needed for message content
|
|
],
|
|
}) as BotClient; // Assert the type here
|
|
|
|
// Define Shoukaku nodes
|
|
const Nodes: NodeOption[] = [
|
|
{
|
|
name: process.env.LAVALINK_NAME || 'lavalink-node-1', // Use an env var or default name
|
|
url: `${process.env.LAVALINK_HOST || 'localhost'}:${process.env.LAVALINK_PORT || 2333}`, // Use || 2333 for default port number
|
|
auth: process.env.LAVALINK_PASSWORD || 'youshallnotpass', // Password from your Lavalink server config
|
|
secure: process.env.LAVALINK_SECURE === 'true', // Set to true if using HTTPS/WSS
|
|
},
|
|
];
|
|
|
|
// Shoukaku options
|
|
const shoukakuOptions: ShoukakuOptions = {
|
|
moveOnDisconnect: false, // Whether to move players to another node when a node disconnects
|
|
resume: true, // Whether to resume players session after Lavalink restarts
|
|
reconnectTries: 3, // Number of attempts to reconnect to Lavalink
|
|
reconnectInterval: 5000, // Interval between reconnect attempts in milliseconds
|
|
// Add other options as needed
|
|
};
|
|
|
|
// Initialize Shoukaku
|
|
client.shoukaku = new Shoukaku(new Connectors.DiscordJS(client), Nodes, shoukakuOptions);
|
|
|
|
// Show the actual Lavalink connection details (without exposing the actual password)
|
|
logger.info(`Lavalink connection configured to: ${process.env.LAVALINK_HOST}:${process.env.LAVALINK_PORT} (Password: ${process.env.LAVALINK_PASSWORD ? '[SET]' : '[NOT SET]'})`);
|
|
|
|
// Collections for commands
|
|
client.commands = new Collection<string, Command>();
|
|
|
|
// --- Command Loading ---
|
|
const commandsPath = path.join(__dirname, 'commands');
|
|
// Read .ts files now
|
|
const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith('.ts'));
|
|
|
|
const loadCommands = async () => {
|
|
for (const file of commandFiles) {
|
|
const filePath = path.join(commandsPath, file);
|
|
try {
|
|
// Use dynamic import for ES Modules/CommonJS interop
|
|
const commandModule = await import(filePath);
|
|
const command: Command = commandModule.default || commandModule; // Handle default exports
|
|
|
|
if (command && typeof command === 'object' && 'data' in command && 'execute' in command) {
|
|
client.commands.set(command.data.name, command);
|
|
logger.info(`Loaded command: ${command.data.name}`);
|
|
} else {
|
|
logger.warn(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property or is not structured correctly.`);
|
|
}
|
|
} catch (error: unknown) { // Type the error as unknown
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.error(`Error loading command at ${filePath}: ${errorMessage}`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- Event Handling ---
|
|
const eventsPath = path.join(__dirname, 'events');
|
|
// Read .ts files now
|
|
const eventFiles = fs.readdirSync(eventsPath).filter((file: string) => file.endsWith('.ts'));
|
|
|
|
const loadEvents = async () => {
|
|
for (const file of eventFiles) {
|
|
const filePath = path.join(eventsPath, file);
|
|
try {
|
|
const eventModule = await import(filePath);
|
|
const event: BotEvent = eventModule.default || eventModule; // Handle default exports
|
|
|
|
if (event && typeof event === 'object' && 'name' in event && 'execute' in event) {
|
|
if (event.once) {
|
|
client.once(event.name, (...args: any[]) => event.execute(...args, client)); // Pass client
|
|
logger.info(`Loaded event ${event.name} (once)`);
|
|
} else {
|
|
client.on(event.name, (...args: any[]) => event.execute(...args, client)); // Pass client
|
|
logger.info(`Loaded event ${event.name}`);
|
|
}
|
|
} else {
|
|
logger.warn(`[WARNING] The event at ${filePath} is missing a required "name" or "execute" property or is not structured correctly.`);
|
|
}
|
|
} catch (error: unknown) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.error(`Error loading event at ${filePath}: ${errorMessage}`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- Shoukaku Event Handling ---
|
|
client.shoukaku.on('ready', (name: string) => logger.info(`Lavalink Node: ${name} is now connected`));
|
|
client.shoukaku.on('error', (name: string, error: Error) => logger.error(`Lavalink Node: ${name} emitted an error: ${error.message}`));
|
|
client.shoukaku.on('close', (name: string, code: number, reason: string | undefined) => 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) => {
|
|
logger.warn(`Lavalink Node: ${name} disconnected. ${count} players were disconnected from this node.`);
|
|
});
|
|
|
|
|
|
// --- Main Execution ---
|
|
async function main() {
|
|
await loadCommands();
|
|
await loadEvents();
|
|
|
|
// Log in to Discord with your client's token
|
|
try {
|
|
await client.login(process.env.DISCORD_TOKEN);
|
|
logger.info('Successfully logged in to Discord.');
|
|
} catch (error: unknown) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.error(`Failed to log in: ${errorMessage}`);
|
|
process.exit(1); // Exit if login fails
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.error(`Error during bot initialization: ${errorMessage}`, error);
|
|
process.exit(1);
|
|
});
|
|
|
|
|
|
// Basic error handling
|
|
process.on('unhandledRejection', (reason: unknown, promise: Promise<any>) => {
|
|
const reasonMessage = reason instanceof Error ? reason.message : String(reason);
|
|
logger.error('Unhandled promise rejection:', { reason: reasonMessage, promise });
|
|
});
|
|
process.on('uncaughtException', (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => {
|
|
logger.error(`Uncaught exception: ${error.message}`, { error, origin });
|
|
// Optional: exit process on critical uncaught exceptions
|
|
// process.exit(1);
|
|
});
|