Add Discord.js bot base with prefix commands and Winston logging
Co-authored-by: yhqe <139268606+yhqe@users.noreply.github.com>
This commit is contained in:
1 parent
8bd000a57a
commit
e8049008eb
12 files changed
+1076
-2
No files matched your search
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
name: 'help',
|
||||
description: 'Lists all available commands or shows info about a specific one.',
|
||||
aliases: ['h', 'commands'],
|
||||
usage: 'help [command]',
|
||||
|
||||
execute(client, message, args) {
|
||||
const prefix = process.env.PREFIX || '!';
|
||||
|
||||
// Show info for a specific command
|
||||
if (args.length > 0) {
|
||||
const name = args[0].toLowerCase();
|
||||
const command = client.commands.get(name);
|
||||
|
||||
if (!command) {
|
||||
return message.reply(`No command found for \`${name}\`.`);
|
||||
}
|
||||
|
||||
const lines = [`**${prefix}${command.name}**`];
|
||||
if (command.description) lines.push(`> ${command.description}`);
|
||||
if (command.aliases?.length)
|
||||
lines.push(`> **Aliases:** ${command.aliases.map((a) => `\`${prefix}${a}\``).join(', ')}`);
|
||||
if (command.usage)
|
||||
lines.push(`> **Usage:** \`${prefix}${command.usage}\``);
|
||||
|
||||
return message.reply(lines.join('\n'));
|
||||
}
|
||||
|
||||
// Build a deduplicated command list (ignore aliases pointing to the same command)
|
||||
const seen = new Set();
|
||||
const commandList = [];
|
||||
|
||||
for (const command of client.commands.values()) {
|
||||
if (seen.has(command.name)) continue;
|
||||
seen.add(command.name);
|
||||
commandList.push(`\`${prefix}${command.name}\` — ${command.description || 'No description.'}`);
|
||||
}
|
||||
|
||||
return message.reply(
|
||||
`**Available commands:**\n${commandList.join('\n')}\n\nUse \`${prefix}help <command>\` for more details.`,
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
name: 'ping',
|
||||
description: 'Replies with Pong! and shows the bot latency.',
|
||||
aliases: ['pong'],
|
||||
usage: 'ping',
|
||||
|
||||
async execute(client, message) {
|
||||
const sent = await message.reply('Pinging…');
|
||||
const latency = sent.createdTimestamp - message.createdTimestamp;
|
||||
const apiLatency = Math.round(client.ws.ping);
|
||||
|
||||
await sent.edit(
|
||||
`🏓 Pong!\n> Bot latency: **${latency}ms** | API latency: **${apiLatency}ms**`,
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('discord.js');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
module.exports = {
|
||||
name: Events.MessageCreate,
|
||||
|
||||
async execute(client, message) {
|
||||
// Ignore bots and messages without the prefix
|
||||
if (message.author.bot) return;
|
||||
|
||||
const prefix = process.env.PREFIX || '!';
|
||||
if (!message.content.startsWith(prefix)) return;
|
||||
|
||||
// Parse command name and arguments
|
||||
const args = message.content.slice(prefix.length).trim().split(/\s+/);
|
||||
const commandName = args.shift().toLowerCase();
|
||||
|
||||
const command = client.commands.get(commandName);
|
||||
if (!command) return;
|
||||
|
||||
logger.info(
|
||||
`[Command] "${commandName}" used by ${message.author.tag} (${message.author.id})` +
|
||||
` in ${message.guild ? `#${message.channel.name} (${message.guild.name})` : 'DMs'}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await command.execute(client, message, args);
|
||||
} catch (err) {
|
||||
logger.error(`[Command] Error executing "${commandName}":`, err);
|
||||
await message
|
||||
.reply('An error occurred while running that command.')
|
||||
.catch(() => null);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('discord.js');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
module.exports = {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
|
||||
execute(client) {
|
||||
logger.info(`[Ready] Logged in as ${client.user.tag} (${client.user.id})`);
|
||||
logger.info(`[Ready] Serving ${client.guilds.cache.size} guild(s).`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Loads all command files from src/commands and registers them on the client.
|
||||
* Each command file must export: { name, description, aliases?, usage?, execute }
|
||||
*
|
||||
* @param {import('discord.js').Client} client
|
||||
*/
|
||||
function loadCommands(client) {
|
||||
const commandsPath = path.join(__dirname, '..', 'commands');
|
||||
const commandFiles = fs
|
||||
.readdirSync(commandsPath)
|
||||
.filter((file) => file.endsWith('.js'));
|
||||
|
||||
let loaded = 0;
|
||||
|
||||
for (const file of commandFiles) {
|
||||
try {
|
||||
const command = require(path.join(commandsPath, file));
|
||||
|
||||
if (!command.name || typeof command.execute !== 'function') {
|
||||
logger.warn(
|
||||
`[CommandHandler] Skipping ${file}: missing "name" or "execute".`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
client.commands.set(command.name.toLowerCase(), command);
|
||||
|
||||
// Register aliases if provided
|
||||
if (Array.isArray(command.aliases)) {
|
||||
for (const alias of command.aliases) {
|
||||
client.commands.set(alias.toLowerCase(), command);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`[CommandHandler] Loaded command: ${command.name}`);
|
||||
loaded++;
|
||||
} catch (err) {
|
||||
logger.error(`[CommandHandler] Failed to load ${file}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[CommandHandler] ${loaded} command(s) loaded.`);
|
||||
}
|
||||
|
||||
module.exports = { loadCommands };
|
||||
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Loads all event files from src/events and registers them on the client.
|
||||
* Each event file must export: { name, once?, execute }
|
||||
*
|
||||
* @param {import('discord.js').Client} client
|
||||
*/
|
||||
function loadEvents(client) {
|
||||
const eventsPath = path.join(__dirname, '..', 'events');
|
||||
const eventFiles = fs
|
||||
.readdirSync(eventsPath)
|
||||
.filter((file) => file.endsWith('.js'));
|
||||
|
||||
let loaded = 0;
|
||||
|
||||
for (const file of eventFiles) {
|
||||
try {
|
||||
const event = require(path.join(eventsPath, file));
|
||||
|
||||
if (!event.name || typeof event.execute !== 'function') {
|
||||
logger.warn(
|
||||
`[EventHandler] Skipping ${file}: missing "name" or "execute".`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.once) {
|
||||
client.once(event.name, (...args) => event.execute(client, ...args));
|
||||
} else {
|
||||
client.on(event.name, (...args) => event.execute(client, ...args));
|
||||
}
|
||||
|
||||
logger.debug(`[EventHandler] Registered event: ${event.name}`);
|
||||
loaded++;
|
||||
} catch (err) {
|
||||
logger.error(`[EventHandler] Failed to load ${file}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[EventHandler] ${loaded} event(s) registered.`);
|
||||
}
|
||||
|
||||
module.exports = { loadEvents };
|
||||
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { Client, Collection, GatewayIntentBits } = require('discord.js');
|
||||
const logger = require('./utils/logger');
|
||||
const { loadCommands } = require('./handlers/commandHandler');
|
||||
const { loadEvents } = require('./handlers/eventHandler');
|
||||
|
||||
// Validate required environment variables
|
||||
if (!process.env.BOT_TOKEN) {
|
||||
logger.error(
|
||||
'BOT_TOKEN is not set. Copy .env.example to .env and fill in your token.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the Discord client with the intents needed for prefix commands
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent, // Required to read message content for prefix commands
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
});
|
||||
|
||||
// Attach a Collection to store commands
|
||||
client.commands = new Collection();
|
||||
|
||||
// Load commands and events
|
||||
loadCommands(client);
|
||||
loadEvents(client);
|
||||
|
||||
// Global error handling
|
||||
process.on('unhandledRejection', (err) => {
|
||||
logger.error('[Process] Unhandled rejection:', err);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error('[Process] Uncaught exception:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Connect to Discord
|
||||
client.login(process.env.BOT_TOKEN).catch((err) => {
|
||||
logger.error('[Login] Failed to log in:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const { createLogger, format, transports } = require('winston');
|
||||
const { combine, timestamp, printf, colorize, errors } = format;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Ensure logs directory exists
|
||||
const logsDir = path.join(__dirname, '..', '..', 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const logFormat = printf(({ level, message, timestamp: ts, stack }) => {
|
||||
return `[${ts}] [${level}]: ${stack || message}`;
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: combine(
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
errors({ stack: true }),
|
||||
logFormat,
|
||||
),
|
||||
transports: [
|
||||
// Human-readable coloured output in the console
|
||||
new transports.Console({
|
||||
format: combine(
|
||||
colorize({ all: true }),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
errors({ stack: true }),
|
||||
logFormat,
|
||||
),
|
||||
}),
|
||||
// All logs at info level and above written to combined.log
|
||||
new transports.File({
|
||||
filename: path.join(logsDir, 'combined.log'),
|
||||
}),
|
||||
// Error logs written to a dedicated file
|
||||
new transports.File({
|
||||
filename: path.join(logsDir, 'error.log'),
|
||||
level: 'error',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
Reference in new issue
Block a user