const { Client, PermissionFlagsBits } = require('discord.js'); const { Collection } = require('@discordjs/collection'); const fs = require('fs'); const path = require('path'); const { stripIndents } = require('common-tags'); const Registry = require('./Registry'); const SlashRegistry = require('./slash/SlashRegistry'); const Dispatcher = require('./Dispatcher'); require('./Extensions'); module.exports = class CommandClient extends Client { constructor(options) { super(options); this.commandPrefix = options.commandPrefix; this.mentionPrefix = typeof options.mentionPrefix === 'undefined' ? true : Boolean(options.mentionPrefix); this.owner = typeof options.owner === 'string' ? [options.owner] : options.owner; this.invite = options.invite || null; this.registry = new Registry(this); this.slashRegistry = new SlashRegistry(this); this.dispatcher = new Dispatcher(this); this.games = new Collection(); this.blacklist = { user: [], guild: [] }; this._throttlingTimeouts = new Map(); this.once('ready', this.onceReady); this.on('messageCreate', this.onMessage); this.on('interactionCreate', this.onInteractionCreate); } isOwner(user) { return this.owner.includes(user.id); } async onceReady() { for (const owner of this.owner) { await this.users.fetch(owner); } } async onMessage(msg) { if (!msg.author) return; if (msg.channel.partial) msg.channel = await this.channels.fetch(msg.channel.id); if (msg.partial) msg = await msg.channel.messages.fetch({ message: msg.id }); if (msg.guild && (!msg.member || msg.member.partial || !msg.guild.members.cache.has(msg.author.id))) { try { await msg.guild.members.fetch(msg.author.id); } catch { // Ignore } } if (msg.author.bot) return; if (this.blacklist.user.includes(msg.author.id)) return; if (msg.guild && this.blacklist.guild.includes(msg.guild.id)) return; if (msg.guild && !msg.channel.permissionsFor(this.user).has(PermissionFlagsBits.SendMessages)) return; const patternCmd = this.dispatcher.isPatternCommand(msg); if (patternCmd) { try { const result = await patternCmd.run(msg); patternCmd.uses++; patternCmd.lastRun = new Date(); this.emit('commandRun', patternCmd, result, msg); } catch (err) { this.emit('commandError', patternCmd, err, msg); await msg.reply(stripIndents` An error occurred while running this command: \`${err.message}\`. You shouldn't ever recieve an error like this. ${this.invite ? `Please visit ${this.invite} for support.` : ''} `); } } if (!this.dispatcher.isCommand(msg)) return; const parsed = await this.dispatcher.parseMessage(msg); if (typeof parsed === 'string') { const helpUsage = this.registry.commands.get('help').usage(); await msg.reply(`${parsed}\n\nUse ${helpUsage} for more information.`); return; } const { command, args } = parsed; if (msg.guild && command.clientPermissions.length) { for (const permission of command.clientPermissions) { if (msg.channel.permissionsFor(this.user).has(permission)) continue; await msg.reply(`The \`${command.name}\` command requires me to have the "${permission}" permission.`); return; } } if (command.guildOnly && !msg.guild) { await msg.reply(`The \`${command.name}\` command can only be used in a server channel.`); return; } if (!this.isOwner(msg.author)) { if (!command._enabled) { await msg.reply(`The \`${command.name}\` command is disabled.`); return; } if (command.ownerOnly) { await msg.reply(`The \`${command.name}\` command can only be used by the bot owner.`); return; } if (command.nsfw && !msg.channel.nsfw) { await msg.reply(`The \`${command.name}\` command can only be used in NSFW channels.`); return; } if (msg.guild && command.userPermissions.length) { for (const permission of command.userPermissions) { if (msg.channel.permissionsFor(msg.author).has(permission)) continue; await msg.reply(`You need the "${permission}" permission to use the \`${command.name}\` command.`); return; } } const throttleAmount = command.throttles.get(msg.author.id); if (throttleAmount && throttleAmount.usages >= command.throttling.usages) { const timeLeft = Math.round((throttleAmount.timeFinish - Date.now()) / 1000); await msg.reply(`Please wait ${timeLeft} seconds before using the \`${command.name}\` command again.`); return; } command.throttles.set(msg.author.id, { usages: (throttleAmount?.usages || 0) + 1, timeFinish: Date.now() + (command.throttling.duration * 1000) }); if (!throttleAmount) { const timeout = setTimeout(() => command.throttles.delete(msg.author.id), command.throttling.duration * 1000); command._timeouts.set(msg.author.id, timeout); } } try { if (command.game) { const current = this.games.get(msg.channel.id); if (current) { await msg.reply(`Please wait until the current game of \`${current.name}\` is finished.`); return; } this.games.set(msg.channel.id, { name: command.name }); } const result = await command.run(msg, args); if (command.game) this.games.delete(msg.channel.id); command.uses++; command.lastRun = new Date(); this.emit('commandRun', command, result, msg, args); } catch (err) { if (command.game) this.games.delete(msg.channel.id); this.emit('commandError', command, err, msg, args); await msg.reply(stripIndents` An error occurred while running this command: \`${err.message}\`. You shouldn't ever recieve an error like this. ${this.invite ? `Please visit ${this.invite} for support.` : ''} `); } } async onInteractionCreate(interaction) { if (!interaction.isChatInputCommand()) return; const command = this.slashRegistry.commands.get(interaction.commandName); if (!command) return; try { const result = await command.run(interaction); this.emit('commandRun', command, result, interaction); } catch (err) { this.emit('commandError', interaction, err); if (interaction.replied || interaction.deferred) { await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); } else { await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); } } } importBlacklist() { const read = fs.readFileSync(path.join(__dirname, '..', 'blacklist.json'), { encoding: 'utf8' }); const file = JSON.parse(read); if (typeof file !== 'object' || Array.isArray(file)) return null; if (!file.guild || !file.user) return null; for (const id of file.guild) { if (typeof id !== 'string') continue; if (this.blacklist.guild.includes(id)) continue; this.blacklist.guild.push(id); } for (const id of file.user) { if (typeof id !== 'string') continue; if (this.blacklist.user.includes(id)) continue; this.blacklist.user.push(id); } return file; } exportBlacklist() { let text = '{\n "guild": [\n '; if (this.blacklist.guild.length) { for (const id of this.blacklist.guild) { text += `"${id}",\n `; } text = text.slice(0, -4); } text += '\n ],\n "user": [\n '; if (this.blacklist.user.length) { for (const id of this.blacklist.user) { text += `"${id}",\n `; } text = text.slice(0, -4); } text += '\n ]\n}\n'; const buf = Buffer.from(text); fs.writeFileSync(path.join(__dirname, '..', 'blacklist.json'), buf, { encoding: 'utf8' }); return buf; } importCommandLeaderboard(add = false) { const read = fs.readFileSync(path.join(__dirname, '..', 'command-leaderboard.json'), { encoding: 'utf8' }); const file = JSON.parse(read); if (typeof file !== 'object' || Array.isArray(file)) return null; for (const [id, value] of Object.entries(file)) { if (typeof value !== 'number') continue; const found = this.registry.commands.get(id); if (!found || found.uses === undefined) continue; if (add) found.uses += value; else found.uses = value; } return file; } exportCommandLeaderboard() { let text = '{'; for (const command of this.registry.commands.values()) { if (command.unknown) continue; if (command.uses === undefined) continue; text += `\n "${command.name}": ${command.uses},`; } text = text.slice(0, -1); text += '\n}\n'; const buf = Buffer.from(text); fs.writeFileSync(path.join(__dirname, '..', 'command-leaderboard.json'), buf, { encoding: 'utf8' }); return buf; } importLastRun() { const read = fs.readFileSync(path.join(__dirname, '..', 'command-last-run.json'), { encoding: 'utf8' }); const file = JSON.parse(read); if (typeof file !== 'object' || Array.isArray(file)) return null; for (const [id, value] of Object.entries(file)) { if (!value) continue; const date = new Date(value); if (date.toString() === 'Invalid Date') continue; const found = this.registry.commands.get(id); if (!found || found.lastRun === undefined) continue; found.lastRun = date; } return file; } exportLastRun() { let text = '{'; for (const command of this.registry.commands.values()) { if (command.unknown) continue; if (command.lastRun === undefined) continue; text += `\n "${command.name}": ${command.lastRun ? `"${command.lastRun.toISOString()}"` : null},`; } text = text.slice(0, -1); text += '\n}\n'; const buf = Buffer.from(text); fs.writeFileSync(path.join(__dirname, '..', 'command-last-run.json'), buf, { encoding: 'utf8' }); return buf; } };