UNPKG

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
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 };