custom-discord-bot
Version:
A powerful, customizable Discord bot framework with embeds, cooldowns, aliases, middleware, function commands, and real-time stats — built with OceanicJS.
318 lines (283 loc) • 12.3 kB
JavaScript
const { Client } = require("oceanic.js");
const fetch = require("node-fetch");
const EventEmitter = require("events");
class DiscordBot extends EventEmitter {
/**
* Creates a new DiscordBot instance.
* @param {Object} options - Bot configuration options.
* @param {string} options.token - Discord bot token.
* @param {string} [options.prefix='!'] - Command prefix.
* @param {Object} [options.commands={}] - Custom command-response pairs (string or embed objects).
* @param {Object} [options.aliases={}] - Command aliases { "alias": "originalCommand" }.
* @param {string} [options.status='online'] - Bot status ('online', 'idle', 'dnd').
* @param {string} [options.statusMessage=''] - Custom status message.
* @param {number} [options.statusType=0] - Activity type (0: Playing, 1: Streaming, 2: Listening, 3: Watching).
* @param {string} [options.avatarUrl=''] - Bot avatar URL.
* @param {string} [options.username=''] - Bot username.
* @param {number} [options.cooldown=0] - Command cooldown in seconds (0 = disabled).
* @param {Function} [options.onReady=null] - Callback when bot is ready.
* @param {Function} [options.onMessage=null] - Middleware for incoming messages.
* @param {Function} [options.onError=null] - Custom error handler.
* @param {boolean} [options.autoReconnect=true] - Auto-reconnect on disconnect.
* @param {boolean} [options.logCommands=true] - Log command usage to console.
*/
constructor(options = {}) {
super();
this.token = options.token;
this.prefix = options.prefix || "!";
this.commands = options.commands || {};
this.aliases = options.aliases || {};
this.status = options.status || "online";
this.statusMessage = options.statusMessage || "";
this.statusType = options.statusType || 0;
this.avatarUrl = options.avatarUrl || "";
this.username = options.username || "";
this.cooldown = (options.cooldown || 0) * 1000;
this.onReady = options.onReady || null;
this.onMessage = options.onMessage || null;
this.onError = options.onError || null;
this.autoReconnect = options.autoReconnect !== false;
this.logCommands = options.logCommands !== false;
this.client = null;
this._cooldowns = new Map();
this._commandStats = new Map();
this._startedAt = null;
}
/**
* Adds a command dynamically at runtime.
* @param {string} name - Command name.
* @param {string|Object} response - Response string or embed object.
* @param {string[]} [cmdAliases=[]] - Optional aliases for the command.
*/
addCommand(name, response, cmdAliases = []) {
this.commands[name.toLowerCase()] = response;
cmdAliases.forEach((alias) => {
this.aliases[alias.toLowerCase()] = name.toLowerCase();
});
if (this.logCommands) console.log(`➕ Command added: ${this.prefix}${name}`);
}
/**
* Removes a command dynamically.
* @param {string} name - Command name to remove.
*/
removeCommand(name) {
delete this.commands[name.toLowerCase()];
Object.keys(this.aliases).forEach((alias) => {
if (this.aliases[alias] === name.toLowerCase()) delete this.aliases[alias];
});
if (this.logCommands) console.log(`➖ Command removed: ${this.prefix}${name}`);
}
/**
* Gets bot uptime in a human-readable format.
* @returns {string} Uptime string.
*/
getUptime() {
if (!this._startedAt) return "Bot is not running.";
const diff = Date.now() - this._startedAt;
const hours = Math.floor(diff / 3600000);
const minutes = Math.floor((diff % 3600000) / 60000);
const seconds = Math.floor((diff % 60000) / 1000);
return `${hours}h ${minutes}m ${seconds}s`;
}
/**
* Gets command usage statistics.
* @returns {Object} Stats object.
*/
getStats() {
return {
uptime: this.getUptime(),
totalCommands: [...this._commandStats.values()].reduce((a, b) => a + b, 0),
commandBreakdown: Object.fromEntries(this._commandStats),
registeredCommands: Object.keys(this.commands).length,
registeredAliases: Object.keys(this.aliases).length,
};
}
/**
* Creates an embed message object.
* @param {Object} options - Embed options.
* @param {string} [options.title] - Embed title.
* @param {string} [options.description] - Embed description.
* @param {number} [options.color=0x5865f2] - Embed color.
* @param {string} [options.thumbnail] - Thumbnail URL.
* @param {string} [options.image] - Image URL.
* @param {Array} [options.fields=[]] - Embed fields.
* @param {Object} [options.footer] - Footer object { text, icon_url }.
* @param {string} [options.url] - Title URL.
* @returns {Object} Embed object ready for Discord.
*/
static createEmbed({ title, description, color = 0x5865f2, thumbnail, image, fields = [], footer, url } = {}) {
const embed = {};
if (title) embed.title = title;
if (description) embed.description = description;
if (color) embed.color = color;
if (thumbnail) embed.thumbnail = { url: thumbnail };
if (image) embed.image = { url: image };
if (fields.length > 0) embed.fields = fields;
if (footer) embed.footer = typeof footer === "string" ? { text: footer } : footer;
if (url) embed.url = url;
embed.timestamp = new Date().toISOString();
return embed;
}
/**
* Starts the Discord bot.
* @returns {Promise<Client>} The Oceanic.js client instance.
*/
async start() {
if (!this.token) {
const err = "❌ ERROR: Bot token is required!";
console.error(err);
this.emit("error", new Error(err));
return;
}
if (this.token === "YOUR_DISCORD_BOT_TOKEN") {
const err = "❌ ERROR: Please replace 'YOUR_DISCORD_BOT_TOKEN' with your actual Discord bot token!";
console.error(err);
this.emit("error", new Error(err));
return;
}
try {
this.client = new Client({
auth: `Bot ${this.token}`,
gateway: {
intents: ["GUILDS", "GUILD_MESSAGES", "MESSAGE_CONTENT"],
autoReconnect: this.autoReconnect,
},
});
this.client.on("ready", async () => {
try {
this._startedAt = Date.now();
console.log(`✅ Bot is online as ${this.client.user.username}`);
this.client.editStatus(this.status, [{ name: this.statusMessage, type: this.statusType }]);
if (this.username && this.client.user.username !== this.username) {
try {
await this.client.user.edit({ username: this.username });
console.log(`🔹 Username set to: ${this.username}`);
} catch (error) {
console.error("❌ Error setting username:", error.message);
}
}
if (this.avatarUrl) {
try {
const response = await fetch(this.avatarUrl);
if (!response.ok) throw new Error(`Failed to fetch avatar. HTTP Status: ${response.status}`);
const buffer = await response.buffer();
await this.client.user.edit({ avatar: `data:image/png;base64,${buffer.toString("base64")}` });
console.log("🌆 Avatar updated successfully!");
} catch (error) {
console.error("❌ Error setting avatar:", error.message);
}
}
console.log("📜 Loaded Commands:");
const cmdKeys = Object.keys(this.commands);
if (cmdKeys.length > 0) {
cmdKeys.forEach((cmd) => {
const isEmbed = typeof this.commands[cmd] === "object";
console.log(` ➜ ${this.prefix}${cmd} ${isEmbed ? "(embed)" : ""}`);
});
} else {
console.log(" ⚠️ No custom commands set.");
}
if (Object.keys(this.aliases).length > 0) {
console.log("🔗 Aliases:");
Object.entries(this.aliases).forEach(([alias, cmd]) => {
console.log(` ➜ ${this.prefix}${alias} → ${this.prefix}${cmd}`);
});
}
this.emit("ready", this.client);
if (this.onReady) this.onReady(this.client);
} catch (error) {
this._handleError("Error during bot initialization", error);
}
});
this.client.on("messageCreate", async (message) => {
try {
if (message.author.bot) return;
// Run middleware if set
if (this.onMessage) {
const proceed = await this.onMessage(message);
if (proceed === false) return;
}
this.emit("message", message);
if (!message.content.startsWith(this.prefix)) return;
let command = message.content.slice(this.prefix.length).trim().split(/\s+/)[0].toLowerCase();
const args = message.content.slice(this.prefix.length).trim().split(/\s+/).slice(1);
// Resolve aliases
if (this.aliases[command]) command = this.aliases[command];
if (!this.commands[command]) return;
// Cooldown check
if (this.cooldown > 0) {
const key = `${message.author.id}-${command}`;
const lastUsed = this._cooldowns.get(key) || 0;
const remaining = this.cooldown - (Date.now() - lastUsed);
if (remaining > 0) {
const secs = (remaining / 1000).toFixed(1);
await message.channel.createMessage({ content: `⏳ Please wait **${secs}s** before using \`${this.prefix}${command}\` again.` });
return;
}
this._cooldowns.set(key, Date.now());
}
// Track stats
this._commandStats.set(command, (this._commandStats.get(command) || 0) + 1);
if (this.logCommands) {
console.log(`⚡ ${message.author.username} used ${this.prefix}${command}${args.length ? ` [${args.join(", ")}]` : ""}`);
}
const response = this.commands[command];
// Handle function commands
if (typeof response === "function") {
await response(message, args);
this.emit("commandUsed", { command, args, user: message.author.username });
return;
}
// Handle embed responses
if (typeof response === "object" && !Array.isArray(response)) {
await message.channel.createMessage({ embeds: [response] });
} else {
// Handle string responses (with placeholder support)
let text = String(response);
text = text.replace("{user}", message.author.mention || message.author.username);
text = text.replace("{server}", message.guild?.name || "DM");
text = text.replace("{args}", args.join(" ") || "");
text = text.replace("{uptime}", this.getUptime());
await message.channel.createMessage({ content: text });
}
this.emit("commandUsed", { command, args, user: message.author.username });
} catch (error) {
this._handleError("Error handling message", error);
}
});
this.client.on("disconnect", () => {
console.log("⚠️ Bot disconnected.");
this.emit("disconnect");
});
this.client.connect();
return this.client;
} catch (error) {
this._handleError("Error creating client", error);
}
}
/**
* Stops the bot gracefully.
*/
stop() {
if (this.client) {
this.client.disconnect(false);
console.log("🛑 Bot stopped.");
this.emit("stop");
}
}
_handleError(context, error) {
console.error(`❌ ${context}:`, error.message);
if (this.onError) this.onError(error, context);
this.emit("error", error);
}
}
/**
* Quick-start function for simple bots (backwards compatible).
* @param {Object} options - Bot configuration options.
*/
function startBot(options) {
const bot = new DiscordBot(options);
bot.start();
return bot;
}
module.exports = { startBot, DiscordBot };