import { Client, ClientOptions, Collection, GuildChannel, GuildMember, Message } from "discord.js"; import config from "./config.json"; import strings from "./clientstrings.json"; import { Sequelize } from "sequelize"; import { CommandList } from "./commands/commandlist"; import { Command } from "./commands/command"; import { BlacklistUser } from "./models/blacklistuser"; import { CommandBlacklist } from "./models/commandblacklist"; import { SlashCommand } from "./slash/slash"; import { SlashCommandList } from "./slash/commandlist"; export class CommandClient extends Client { constructor (options: ClientOptions, db: Sequelize) { super(options); process.on('exit', this.destroy); process.on('SIGINT', this.destroy); this.db = db; for (const command of CommandList) { this.commands.set(command.name, command); } for (const command of SlashCommandList) { this.slashCommands.set(command.name, command); } this.on('messageCreate', async (msg: Message) => { if (!msg.content.startsWith(config.prefix) || msg.author.bot) return; if (!config.owners.includes(msg.author.id) && (await BlacklistUser.findOne({ where: { user_id: msg.author.id } }))) return; this.parseCommand(msg); }); this.on('ready', () => { console.log(`Logged in as ${this.user!.tag}`); }); this.on('interactionCreate', async (int) => { if (int.isCommand()) { if (!int.channel) return; const command = this.slashCommands.get(int.commandName); if (!command) throw new Error('Slash command does not exist'); if (!int.guild && command.guildOnly) { int.reply({ content: strings.command.guildOnly, ephemeral: true }); return; } if (command.permission && !config.owners.includes(int.user.id)) { if (!(int.channel instanceof GuildChannel)) { int.reply({ content: strings.command.guildOnly, ephemeral: true }); return; } for (const permission of command.permission) { if (!(int.member as GuildMember).permissionsIn(int.channel).has(permission)){ int.reply({ content: strings.command.noPermission, ephemeral: true }); return; } } } command.execute(int).catch(error => { int.reply({ content: strings.error, ephemeral: true }) .catch(error => console.error(error)); return console.error(error); }); return console.info(`${int.user.tag} (${int.user.id}) used ${command.name} in ${'name' in int.channel ? int.channel.name : 'DM CHANNEL'} (${int.channel.id})`); } else if (int.isAutocomplete()) { if (!int.channel) return; const command = this.slashCommands.get(int.commandName); if (!command) throw new Error('Slash command does not exist'); command.autocomplete?.(int).catch(error => { return console.error(error); }); } }); this.login(config.token); } /** * Gives an array of single or multi-word arguments * @param msg * @returns */ static formatArgs (msg: string): string[] { const args = msg.match(/("[^"]+")|(\S+)/g); if (args === null) return []; for (let [i, arg] of args.entries()) { args[i] = arg.replace(/(^"|"$)/g, ''); } return args; } async parseCommand (msg: Message): Promise { const args = CommandClient.formatArgs(msg.content.slice(config.prefix.length)); if (args.length < 1) return; const commandName = args.shift()!.toLowerCase(); const command: Command | undefined = this.commands.get(commandName) || this.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName)); if (!command) { this.nonCommandParse(); return; }; if (command.ownerOnly && !config.owners.includes(msg.author.id)) { msg.reply(strings.command.ownerOnly); return; } if (msg.channel instanceof GuildChannel) { if (!msg.member) throw new Error('Command sending member does not exist'); if (!config.owners.includes(msg.author.id)) { if (command.permission) { for (const permission of command.permission) { if (!msg.member.permissionsIn(msg.channel).has(permission)) { msg.reply(strings.command.noPermission); return; } } } const member = (await CommandBlacklist.findOrCreate({ where: { user_id: msg.author.id, guild_id: msg.guild!.id } }))[0]; if (JSON.parse(member.blacklist)[command.name]) { return; } } } if(command.guildOnly && !(msg.channel instanceof GuildChannel)) { msg.reply(strings.command.guildOnly); return; } if (command.args.length > 0 && !command.args[0].optional && args.length === 0) { let reply = strings.command.noArguments; if (command.usage) { reply += `\n${strings.command.usagePrefix} \`${config.prefix}${command.name} ${command.usage}\``; } msg.channel.send(reply); return; } if (args.length < command.args.filter(arg => {return !arg.optional}).length) { let reply = strings.command.missingArguments; if (command.usage) { reply += `\n${strings.command.usagePrefix} \`${config.prefix}${command.name} ${command.usage}\``; } msg.channel.send(reply); return; } if (command.cooldown && !config.owners.includes(msg.author.id)) { if (!this.cooldowns.has(command.name)) { this.cooldowns.set(command.name, new Collection()); } const now = Date.now(); const timestamps = this.cooldowns.get(command.name); if (!timestamps) throw new Error('Command lacks cooldown collection'); if (timestamps.has(msg.author.id)) { const expirationTime = (timestamps.get(msg.author.id) as number) + command.cooldown; const timeLeft = (expirationTime - now) / 1000; msg.reply(strings.command.onCooldown.replace('{1}', timeLeft.toFixed(1)).replace('{2}', command.name)); return; } timestamps.set(msg.author.id, now); setTimeout(() => timestamps.delete(msg.author.id), command.cooldown); } try { return this.runCommand(msg, command, args); } catch (error) { console.error(error); msg.reply(strings.error); } } nonCommandParse () { return; } runCommand (msg: Message, command: Command, args: string[]): Command | undefined { if (command.args.length > 0 && !command.args[command.args.length-1].infinite) { args[command.args.length-1] = args.slice(command.args.length-1, args.length).join(' '); } const parsedArgs: any[] = []; for(var i = 0; i < command.args.length; i++) { try { if (!command.args[i].infinite) { if (!command.args[i].type.validate(args[i], msg)) throw new Error('Argument is invalid'); parsedArgs[i] = command.args[i].type.parse(args[i], msg); } else { const infinite = []; for (var j = i; j < args.length; j++) { if (!command.args[i].type.validate(args[j], msg)) throw new Error('Argument is invalid'); infinite.push(command.args[i].type.parse(args[j], msg)); } parsedArgs[i] = infinite; } if (command.args[i].validator && !command.args[i].validator?.(args[i], msg)) throw new Error('Argument is invalid'); } catch { msg.reply(strings.command.invalidArgument.replace('{1}', args[i])); return; } } const keyedArgs: { [key: string]: any } = {}; for (var i = 0; i < command.args.length; i++) { keyedArgs[command.args[i].key] = parsedArgs[i]; } console.info(`${msg.author.tag} (${msg.author.id}) used ${command.name} in ${'name' in msg.channel ? msg.channel.name : 'DM CHANNEL'} (${msg.channel.id})`); command.execute(msg, keyedArgs); return command; } readonly commands = new Collection(); readonly slashCommands = new Collection(); readonly cooldowns = new Collection>(); db: Sequelize }