From 7917766ce386e7649ffa2a840bd69b20e9d61b2d Mon Sep 17 00:00:00 2001 From: Dragon Fire Date: Sat, 5 Jun 2021 12:17:33 -0400 Subject: [PATCH] Framework Rewrite --- Xiao.js | 37 +--- assets/json/april-fools.json | 17 -- assets/json/permission-names.json | 35 ++++ commands/auto/no-u.js | 21 --- commands/auto/unflip.js | 17 -- commands/info/role.js | 2 +- commands/util-public/help.js | 4 +- commands/util-public/info.js | 3 +- commands/util-public/prefix.js | 6 +- commands/util-public/unknown-command.js | 2 +- commands/util/eval.js | 116 ++++++++++++ framework/Argument.js | 20 +++ framework/ArgumentType.js | 17 ++ framework/Client.js | 228 ++++++++++++++++++++++++ framework/Command.js | 52 ++++++ framework/Dispatcher.js | 58 ++++++ framework/Extensions.js | 21 +++ framework/Group.js | 12 ++ framework/Registry.js | 74 ++++++++ framework/UnionType.js | 37 ++++ framework/types/boolean.js | 21 +++ framework/types/channel.js | 39 ++++ framework/types/command.js | 17 ++ framework/types/custom-emoji.js | 40 +++++ framework/types/default-emoji.js | 18 ++ framework/types/float.js | 20 +++ framework/types/group.js | 17 ++ framework/types/integer.js | 20 +++ framework/types/member.js | 51 ++++++ framework/types/message.js | 16 ++ framework/types/role.js | 39 ++++ framework/types/string.js | 18 ++ framework/types/user.js | 53 ++++++ package.json | 8 +- structures/Client.js | 111 +----------- structures/Command.js | 32 ---- structures/commands/AutoReply.js | 4 +- structures/commands/Subreddit.js | 6 +- types/code.js | 4 +- types/font.js | 12 +- types/image-or-avatar.js | 24 +-- types/image.js | 20 +-- types/month.js | 4 +- types/pokemon.js | 4 +- types/sherlock.js | 6 +- types/timezone.js | 4 +- types/url.js | 4 +- util/Util.js | 4 + 48 files changed, 1101 insertions(+), 294 deletions(-) delete mode 100644 assets/json/april-fools.json create mode 100644 assets/json/permission-names.json delete mode 100644 commands/auto/no-u.js delete mode 100644 commands/auto/unflip.js create mode 100644 commands/util/eval.js create mode 100644 framework/Argument.js create mode 100644 framework/ArgumentType.js create mode 100644 framework/Client.js create mode 100644 framework/Command.js create mode 100644 framework/Dispatcher.js create mode 100644 framework/Extensions.js create mode 100644 framework/Group.js create mode 100644 framework/Registry.js create mode 100644 framework/UnionType.js create mode 100644 framework/types/boolean.js create mode 100644 framework/types/channel.js create mode 100644 framework/types/command.js create mode 100644 framework/types/custom-emoji.js create mode 100644 framework/types/default-emoji.js create mode 100644 framework/types/float.js create mode 100644 framework/types/group.js create mode 100644 framework/types/integer.js create mode 100644 framework/types/member.js create mode 100644 framework/types/message.js create mode 100644 framework/types/role.js create mode 100644 framework/types/string.js create mode 100644 framework/types/user.js delete mode 100644 structures/Command.js diff --git a/Xiao.js b/Xiao.js index b46d9012..c8fbe5bc 100644 --- a/Xiao.js +++ b/Xiao.js @@ -1,8 +1,8 @@ require('dotenv').config(); -const { XIAO_TOKEN, OWNERS, XIAO_PREFIX, INVITE, APRIL_FOOLS } = process.env; +const { XIAO_TOKEN, OWNERS, XIAO_PREFIX, INVITE } = process.env; const { mkdir } = require('fs/promises'); const path = require('path'); -const { Intents, Permissions, SystemChannelFlags, MessageEmbed } = require('discord.js'); +const { Intents, MessageEmbed } = require('discord.js'); const Client = require('./structures/Client'); const client = new Client({ commandPrefix: XIAO_PREFIX, @@ -16,7 +16,6 @@ const client = new Client({ intents: [Intents.NON_PRIVILEGED, Intents.FLAGS.GUILD_MEMBERS] }); const { formatNumber, checkFileExists } = require('./util/Util'); -const aprilFoolsMsgs = require('./assets/json/april-fools'); client.registry .registerDefaultTypes() @@ -52,13 +51,6 @@ client.registry ['roleplay', 'Roleplay'], ['other', 'Other'] ]) - .registerDefaultCommands({ - help: false, - ping: false, - prefix: false, - commandState: false, - unknownCommand: false - }) .registerCommandsIn(path.join(__dirname, 'commands')); client.on('ready', async () => { @@ -248,7 +240,7 @@ client.on('message', async msg => { const hasEmbed = msg.embeds.length !== 0; if (msg.author.bot || (!hasText && !hasImage && !hasEmbed)) return; if (client.blacklist.user.includes(msg.author.id)) return; - if (msg.isCommand && msg.channel.type !== 'dm') return; + if (client.dispatcher.isCommand(msg) && msg.channel.type !== 'dm') return; if (client.games.has(msg.channel.id)) return; // Cleverbot handler @@ -340,8 +332,8 @@ client.on('guildDelete', async guild => { client.on('guildMemberRemove', async member => { if (member.id === client.user.id) return null; const channel = member.guild.systemChannel; - if (!channel || !channel.permissionsFor(client.user).has(Permissions.FLAGS.SEND_MESSAGES)) return null; - if (member.guild.systemChannelFlags.has(SystemChannelFlags.FLAGS.SUPPRESS_JOIN_NOTIFICATIONS)) return null; + if (!channel || !channel.permissionsFor(client.user).has('SEND_MESSAGES')) return null; + if (member.guild.systemChannelFlags.has('SUPPRESS_JOIN_NOTIFICATIONS')) return null; if (channel.topic && channel.topic.includes('')) return null; try { const leaveMessage = client.leaveMessages[Math.floor(Math.random() * client.leaveMessages.length)]; @@ -387,30 +379,11 @@ client.on('warn', warn => client.logger.warn(warn)); client.on('commandRun', async command => { if (command.unknown) return; client.logger.info(`[COMMAND] ${command.name} was used.`); - if (command.uses === undefined) return; - command.uses++; - if (command.lastRun === undefined) return; - command.lastRun = new Date(); const channel = await client.fetchCommandChannel(); channel.send(`\`${command.name}\` was used! It has now been used **${formatNumber(command.uses)}** times!`) .catch(() => null); }); -client.dispatcher.addInhibitor(msg => { - if (client.blacklist.user.includes(msg.author.id)) return 'blacklisted'; - if (msg.guild && client.blacklist.guild.includes(msg.guild.id)) return 'blacklisted'; - return false; -}); - -if (APRIL_FOOLS) { - client.dispatcher.addInhibitor(msg => { - if (client.isOwner(msg.author)) return false; - const random = Math.floor(Math.random() * 2); - if (random === 1) return msg.reply(aprilFoolsMsgs[Math.floor(Math.random() * aprilFoolsMsgs.length)]); - return false; - }); -} - client.on('commandError', (command, err) => client.logger.error(`[COMMAND:${command.name}]\n${err.stack}`)); client.login(XIAO_TOKEN); diff --git a/assets/json/april-fools.json b/assets/json/april-fools.json deleted file mode 100644 index 61ae5785..00000000 --- a/assets/json/april-fools.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - "You don't command me!", - "Sorry loser, I don't listen to idiots.", - "I take orders from no one!", - "Hahaha look at you, trying to use a command.", - "You wish you could use a bot like me.", - "I'm too cute to take orders from you.", - "You? _You_? As if I'd do anything for _you_.", - "I've got better things to do.", - "Go away.", - "Keep trying. Maybe one day it'll work.", - "Nope, sorry, I'm not listening.", - "La la la la la, I'm not listening!", - "Eat pant.", - "Command? What's that? Is it tasty?", - "Go do something more productive with your life." -] diff --git a/assets/json/permission-names.json b/assets/json/permission-names.json new file mode 100644 index 00000000..dfc6d828 --- /dev/null +++ b/assets/json/permission-names.json @@ -0,0 +1,35 @@ +{ + "ADMINISTRATOR": "", + "CREATE_INSTANT_INVITE": "", + "KICK_MEMBERS": "", + "BAN_MEMBERS": "", + "MANAGE_CHANNELS": "", + "MANAGE_GUILD": "", + "ADD_REACTIONS": "", + "VIEW_AUDIT_LOG": "", + "PRIORITY_SPEAKER": "", + "STREAM": "", + "VIEW_CHANNEL": "", + "SEND_MESSAGES": "", + "SEND_TTS_MESSAGES": "", + "MANAGE_MESSAGES": "", + "EMBED_LINKS": "", + "ATTACH_FILES": "", + "READ_MESSAGE_HISTORY": "", + "MENTION_EVERYONE": "", + "USE_EXTERNAL_EMOJIS": "", + "VIEW_GUILD_INSIGHTS": "", + "CONNECT": "", + "SPEAK": "", + "MUTE_MEMBERS": "", + "DEAFEN_MEMBERS": "", + "MOVE_MEMBERS": "", + "USE_VAD": "", + "CHANGE_NICKNAME": "", + "MANAGE_NICKNAMES": "", + "MANAGE_ROLES": "", + "MANAGE_WEBHOOKS": "", + "MANAGE_EMOJIS": "", + "USE_APPLICATION_COMMANDS": "", + "REQUEST_TO_SPEAK": "" +} diff --git a/commands/auto/no-u.js b/commands/auto/no-u.js deleted file mode 100644 index 86efa4e1..00000000 --- a/commands/auto/no-u.js +++ /dev/null @@ -1,21 +0,0 @@ -const Command = require('../../structures/commands/AutoReply'); - -module.exports = class NoUCommand extends Command { - constructor(client) { - super(client, { - name: 'no-u', - aliases: ['no-you'], - group: 'auto', - memberName: 'no-u', - description: 'no u', - patterns: [/^n+o+ u+$/i] - }); - } - - generateText(fromPattern) { - if (!fromPattern) return 'no u'; - const chance = Boolean(Math.floor(Math.random() * 2)); - if (chance) return null; - return 'no u'; - } -}; diff --git a/commands/auto/unflip.js b/commands/auto/unflip.js deleted file mode 100644 index bd5a6853..00000000 --- a/commands/auto/unflip.js +++ /dev/null @@ -1,17 +0,0 @@ -const Command = require('../../structures/commands/AutoReply'); - -module.exports = class UnflipCommand extends Command { - constructor(client) { - super(client, { - name: 'unflip', - group: 'auto', - memberName: 'unflip', - description: 'Unflips a flipped table.', - patterns: [/\(╯°□°)╯︵ ┻━┻/i] - }); - } - - generateText() { - return '┬─┬ ノ( ゜-゜ノ)'; - } -}; diff --git a/commands/info/role.js b/commands/info/role.js index cc9584f2..c17ab71f 100644 --- a/commands/info/role.js +++ b/commands/info/role.js @@ -1,7 +1,7 @@ const Command = require('../../structures/Command'); const moment = require('moment'); const { MessageEmbed } = require('discord.js'); -const { util: { permissions } } = require('discord.js-commando'); +const permissions = require('../../assets/json/permission-names'); module.exports = class RoleCommand extends Command { constructor(client) { diff --git a/commands/util-public/help.js b/commands/util-public/help.js index a2f2b04b..db94fb4b 100644 --- a/commands/util-public/help.js +++ b/commands/util-public/help.js @@ -1,6 +1,6 @@ const Command = require('../../structures/Command'); const { MessageEmbed } = require('discord.js'); -const { util: { permissions } } = require('discord.js-commando'); +const permissions = require('../../assets/json/permission-names'); const { stripIndents } = require('common-tags'); module.exports = class HelpCommand extends Command { @@ -31,7 +31,7 @@ module.exports = class HelpCommand extends Command { const embed = new MessageEmbed() .setTitle(`Command List (Page ${i + 1})`) .setDescription(stripIndents` - To run a command, use ${msg.anyUsage('')}. + To run a command, use ${this.usage()}. ${nsfw ? '' : 'Use in an NSFW channel to see NSFW commands.'} `) .setColor(0x00AE86); diff --git a/commands/util-public/info.js b/commands/util-public/info.js index 5e2292fc..ed315279 100644 --- a/commands/util-public/info.js +++ b/commands/util-public/info.js @@ -1,6 +1,5 @@ const Command = require('../../structures/Command'); const { MessageEmbed, version: djsVersion } = require('discord.js'); -const { version: commandoVersion } = require('discord.js-commando'); const moment = require('moment'); require('moment-duration-format'); const { formatNumber, embedURL } = require('../../util/Util'); @@ -39,7 +38,7 @@ module.exports = class InfoCommand extends Command { .addField('❯ Version', `v${version}`, true) .addField('❯ Node.js', process.version, true) .addField('❯ Discord.js', `v${djsVersion}`, true) - .addField('❯ Commando', `v${commandoVersion}`, true) + .addField('❯ Framework', 'Custom', true) .addField('❯ Dependencies', Object.keys(deps).sort().join(', ')); return msg.embed(embed); } diff --git a/commands/util-public/prefix.js b/commands/util-public/prefix.js index c3207950..db7872cf 100644 --- a/commands/util-public/prefix.js +++ b/commands/util-public/prefix.js @@ -1,5 +1,4 @@ const Command = require('../../structures/Command'); -const { stripIndents } = require('common-tags'); module.exports = class PrefixCommand extends Command { constructor(client) { @@ -14,9 +13,6 @@ module.exports = class PrefixCommand extends Command { run(msg) { const prefix = msg.guild ? msg.guild.commandPrefix : this.client.commandPrefix; - return msg.reply(stripIndents` - ${prefix ? `The command prefix is \`${prefix}\`.` : 'There is no command prefix.'} - To run a command, use ${msg.anyUsage('')}. - `); + return msg.reply(prefix ? `The command prefix is \`${prefix}\`.` : 'There is no command prefix.'); } }; diff --git a/commands/util-public/unknown-command.js b/commands/util-public/unknown-command.js index 94544650..16632925 100644 --- a/commands/util-public/unknown-command.js +++ b/commands/util-public/unknown-command.js @@ -18,7 +18,7 @@ module.exports = class UnknownCommandCommand extends Command { run(msg) { if (!msg.guild) return null; const commands = this.makeCommandArray(this.client.isOwner(msg.author), msg.channel.nsfw); - const command = msg.content.match(this.client.dispatcher._commandPatterns[this.client.commandPrefix]); + const command = msg.content.match(this.client.dispatcher.commandPattern); const str = command ? command[2] : msg.content.split(' ')[0]; const results = didYouMean(str, commands, { returnType: ReturnTypeEnums.ALL_SORTED_MATCHES }); return msg.reply(stripIndents` diff --git a/commands/util/eval.js b/commands/util/eval.js new file mode 100644 index 00000000..a21c4d21 --- /dev/null +++ b/commands/util/eval.js @@ -0,0 +1,116 @@ +// Credit: https://github.com/discordjs/Commando/blob/master/src/commands/util/eval.js +const util = require('util'); +const discord = require('discord.js'); +const tags = require('common-tags'); +const { escapeRegex } = require('../../util/Util'); +const Command = require('../../structures/Command'); + +const nl = '!!NL!!'; +const nlPattern = new RegExp(nl, 'g'); + +module.exports = class EvalCommand extends Command { + constructor(client) { + super(client, { + name: 'eval', + group: 'util', + memberName: 'eval', + description: 'Executes JavaScript code.', + details: 'Only the bot owner(s) may use this command.', + ownerOnly: true, + args: [ + { + key: 'script', + prompt: 'What code would you like to evaluate?', + type: 'string' + } + ] + }); + + this.lastResult = null; + Object.defineProperty(this, '_sensitivePattern', { value: null, configurable: true }); + } + + run(msg, args) { + // Make a bunch of helpers + /* eslint-disable no-unused-vars */ + const message = msg; + const client = msg.client; + const lastResult = this.lastResult; + const doReply = val => { + if (val instanceof Error) { + msg.reply(`Callback error: \`${val}\``); + } else { + const result = this.makeResultMessages(val, process.hrtime(this.hrStart)); + if (Array.isArray(result)) { + for(const item of result) msg.reply(item); + } else { + msg.reply(result); + } + } + }; + /* eslint-enable no-unused-vars */ + + // Remove any surrounding code blocks before evaluation + if (args.script.startsWith('```') && args.script.endsWith('```')) { + args.script = args.script.replace(/(^.*?\s)|(\n.*$)/g, ''); + } + + // Run the code and measure its execution time + let hrDiff; + try { + const hrStart = process.hrtime(); + this.lastResult = eval(args.script); + hrDiff = process.hrtime(hrStart); + } catch (err) { + return msg.reply(`Error while evaluating: \`${err}\``); + } + + // Prepare for callback time and respond + this.hrStart = process.hrtime(); + const result = this.makeResultMessages(this.lastResult, hrDiff, args.script); + if (Array.isArray(result)) { + return result.map(item => msg.reply(item)); + } else { + return msg.reply(result); + } + } + + makeResultMessages(result, hrDiff, input = null) { + const inspected = util.inspect(result, { depth: 0 }) + .replace(nlPattern, '\n') + .replace(this.sensitivePattern, '--snip--'); + const split = inspected.split('\n'); + const last = inspected.length - 1; + const prependPart = inspected[0] !== '{' && inspected[0] !== '[' && inspected[0] !== "'" ? split[0] : inspected[0]; + const appendPart = inspected[last] !== '}' && inspected[last] !== ']' && inspected[last] !== "'" ? + split[split.length - 1] : + inspected[last]; + const prepend = `\`\`\`javascript\n${prependPart}\n`; + const append = `\n${appendPart}\n\`\`\``; + if (input) { + return discord.splitMessage(tags.stripIndents` + *Executed in ${hrDiff[0] > 0 ? `${hrDiff[0]}s ` : ''}${hrDiff[1] / 1000000}ms.* + \`\`\`javascript + ${inspected} + \`\`\` + `, { maxLength: 1900, prepend, append }); + } else { + return discord.splitMessage(tags.stripIndents` + *Callback executed after ${hrDiff[0] > 0 ? `${hrDiff[0]}s ` : ''}${hrDiff[1] / 1000000}ms.* + \`\`\`javascript + ${inspected} + \`\`\` + `, { maxLength: 1900, prepend, append }); + } + } + + get sensitivePattern() { + if (!this._sensitivePattern) { + const client = this.client; + let pattern = ''; + if (client.token) pattern += escapeRegex(client.token); + Object.defineProperty(this, '_sensitivePattern', { value: new RegExp(pattern, 'gi'), configurable: false }); + } + return this._sensitivePattern; + } +}; diff --git a/framework/Argument.js b/framework/Argument.js new file mode 100644 index 00000000..f3eb0170 --- /dev/null +++ b/framework/Argument.js @@ -0,0 +1,20 @@ +const UnionType = require('./UnionType'); + +module.exports = class Argument { + constructor(client, options) { + Object.defineProperty(this, 'client', { value: client }); + + this.key = options.key.toLowerCase(); + this.typeID = options.type.toLowerCase(); + this.min = options.min; + this.max = options.max; + this.oneOf = options.oneOf; + this.default = options.default; + this.avatarSize = options.avatarSize || 2048; + } + + get type() { + if (this.typeID.includes('|')) return new UnionType(this.client, this.typeID); + return this.client.registry.types.get(this.typeID); + } +}; diff --git a/framework/ArgumentType.js b/framework/ArgumentType.js new file mode 100644 index 00000000..77f29c0a --- /dev/null +++ b/framework/ArgumentType.js @@ -0,0 +1,17 @@ +module.exports = class ArgumentType { + constructor(id) { + this.id = id.toLowerCase(); + } + + validate(val) { + return Boolean(val); + } + + parse(val) { + return val; + } + + isEmpty(val) { + return !val; + } +}; diff --git a/framework/Client.js b/framework/Client.js new file mode 100644 index 00000000..f6872fc2 --- /dev/null +++ b/framework/Client.js @@ -0,0 +1,228 @@ +const { Client } = require('discord.js'); +const fs = require('fs'); +const { stripIndents } = require('common-tags'); +const Registry = require('./Registry'); +const Dispatcher = require('./Dispatcher'); +const Patreon = require('../structures/Patreon'); +require('./Extensions'); + +module.exports = class CommandClient extends Client { + constructor(options) { + super(options); + + this.commandPrefix = options.commandPrefix; + this.owner = typeof options.owner === 'string' ? [options.owner] : options.owner; + this.invite = options.invite || null; + this.registry = new Registry(this); + this.dispatcher = new Dispatcher(this); + this.patreon = new Patreon(); + this.blacklist = { user: [], guild: [] }; + this._throttlingTimeouts = new Map(); + + this.once('ready', this.onceReady); + this.on('message', this.onMessage); + } + + isOwner(user) { + return this.owners.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(msg.id); + if (msg.member.partial || !msg.guild.members.cache.has(msg.author.id)) { + await msg.guild.members.fetch(msg.author.id); + } + + 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.channel.permissionsFor(this.user).has('SEND_MESSAGES')) return; + 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} Use ${helpUsage} for more information.`); + return; + } + const { command, args } = parsed; + if (!command._enabled) { + await msg.reply(`The \`${command.name}\` command is disabled.`); + return; + } + if (command.ownerOnly && !this.isOwner(msg.author)) { + await msg.reply(`The \`${command.name}\` command can only be used by the bot owner.`); + return; + } + if (command.guildOnly && !msg.guild) { + await msg.reply(`The \`${command.name}\` command can only be used in a server channel.`); + return; + } + if (command.nsfw && !msg.channel.nsfw) { + await msg.reply(`The \`${command.name}\` command can only be used in NSFW channels.`); + return; + } + if (command.patronOnly && !this.patreon.isPatron(msg.author.id)) { + await msg.reply(stripIndents` + The \`${command.name}\` command can only be used by Patrons. + Visit to sign-up! + `); + return; + } + if (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.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) || 0; + if (throttleAmount >= command.throttling.uses) { + const timeout = command._timeouts.get(msg.author.id); + await msg.reply(`Please wait ${getTimeLeft(timeout)} seconds before using the \`${command.name}\` command again.`); + return; + } + command.throttles.set(msg.author.id, throttleAmount + 1); + if (!throttleAmount) { + const timeout = setTimeout(() => command.throttles.delete(msg.author.id), command.throttling.duration); + command._timeouts.set(msg.author.id, timeout); + } + try { + const result = await command.run(msg, args); + command.uses++; + command.lastRun = new Date(); + this.emit('commandRun', command, result, msg, args); + } catch (err) { + 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.` : ''} + `); + } + } + + 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; + } +}; + +function getTimeLeft(timeout) { + return Math.ceil((timeout._idleStart + timeout._idleTimeout - Date.now()) / 1000); +} diff --git a/framework/Command.js b/framework/Command.js new file mode 100644 index 00000000..228b1659 --- /dev/null +++ b/framework/Command.js @@ -0,0 +1,52 @@ +const Argument = require('./Argument'); + +module.exports = class Command { + constructor(client, options) { + Object.defineProperty(this, 'client', { value: client }); + + this.name = options.name.toLowerCase(); + this.aliases = options.aliases ? options.aliases.map(alias => alias.toLowerCase()) : []; + this.groupID = options.group.toLowerCase(); + this.memberName = options.memberName.toLowerCase(); + this.description = options.description; + this.details = options.details || null; + this.args = options.args ? options.args.map(arg => new Argument(arg)) : []; + this.clientPermissions = options.clientPermissions || []; + this.userPermissions = options.userPermissions || []; + this.ownerOnly = options.ownerOnly || false; + this.nsfw = options.nsfw || false; + this.guildOnly = options.guildOnly || false; + this.patronOnly = options.patronOnly || false; + this.guarded = options.guarded || false; + this.throttling = options.throttling || { usages: 2, duration: 5 }; + this.credit = options.credit || []; + this.credit.push({ + name: 'Dragon Fire', + url: 'https://github.com/dragonfire535', + reason: 'Code' + }); + this.uses = 0; + this.lastRun = null; + this.throttles = new Map(); + this._timeouts = new Map(); + this._enabled = true; + } + + get group() { + return this.client.registry.groups.get(this.groupID); + } + + usage() { + const args = this.args + .map(arg => `${arg.default ? '[' : '<'}${arg.label || arg.name}${arg.default ? ']' : '>'}`).join(' '); + return `\`${this.client.commandPrefix}${this.name} ${args}\` or \`@${this.client.user.tag} ${this.name} ${args}\``; + } + + disable() { + this._enabled = false; + } + + enable() { + this._enabled = true; + } +}; diff --git a/framework/Dispatcher.js b/framework/Dispatcher.js new file mode 100644 index 00000000..fe47d989 --- /dev/null +++ b/framework/Dispatcher.js @@ -0,0 +1,58 @@ +const minimist = require('minimist'); +const argRegex = /"([^"]*)"|(\S+)/g; + +module.exports = class CommandDispatcher { + constructor(client) { + Object.defineProperty(this, 'client', { value: client }); + + this._commandPattern = null; + } + + get commandPattern() { + if (this._commandPattern) return this._commandPattern; + const prefix = this.client.commandPrefix; + this._commandPattern = new RegExp( + `^(<@!?${this.client.user.id}>\\s+(?:${prefix}}\\s*)?|${prefix}\\s*)([^\\s]+)`, 'i' + ); + return this._commandPattern; + } + + isCommand(msg) { + const command = msg.content.match(this.commandPattern); + return Boolean(command); + } + + async parseMessage(msg) { + const command = this.resolveCommand(command[2].toLowerCase()); + if (!command) { + return { + command: this.registry.commands.find(cmd => cmd.unknown), + args: { command: command[2].toLowerCase() } + }; + } + const content = msg.content.replace(this.commandPattern, '').trim(); + const result = (content.match(argRegex) || []).map(m => m.replace(argRegex, '$1$2')); + const parsed = minimist(result); + const result = { flags: [...parsed] }; + for (let i = 0; i > command.args.length; i++) { + const arg = command.args[i]; + const parsedArg = result._[i]; + if (arg.isEmpty(parsedArg, msg, arg)) { + if (arg.default) { + result[arg.name] = typeof arg.default === 'function' ? arg.default(msg) : arg.default; + continue; + } else { + return `The "${arg.label || arg.name}" argument is required.`; + } + } + const valid = await arg.validate(parsedArg, msg, arg); + if (!valid) return `An invalid value was provided for the "${arg.label || arg.name}" argument.`; + result[arg.name] = await arg.parse(parsedArg, msg, arg); + } + return { command, args: result }; + } + + resolveCommand(command) { + return this.registry.commands.find(cmd => cmd.name === command || cmd.aliases.includes(command)); + } +}; diff --git a/framework/Extensions.js b/framework/Extensions.js new file mode 100644 index 00000000..02d509e9 --- /dev/null +++ b/framework/Extensions.js @@ -0,0 +1,21 @@ +const { Structures } = require('discord.js'); + +module.exports = Structures.extend('Message', Message => { + return class CommandMessage extends Message { + constructor(...args) { + super(...args); + } + + say(content, options) { + return this.channel.send(content, options); + } + + embed(embed, options) { + return this.channel.send('', { embed, ...options }); + } + + code(lang, content, options) { + return this.channel.send(content, { code: lang, ...options }); + } + } +}); diff --git a/framework/Group.js b/framework/Group.js new file mode 100644 index 00000000..3378ac0e --- /dev/null +++ b/framework/Group.js @@ -0,0 +1,12 @@ +module.exports = class Group { + constructor(client, id, name) { + Object.defineProperty(this, 'client', { value: client }); + + this.id = id.toLowerCase(); + this.name = name; + } + + get commands() { + return this.client.registry.commands.filter(command => command.groupID === this.id); + } +}; diff --git a/framework/Registry.js b/framework/Registry.js new file mode 100644 index 00000000..df0e6650 --- /dev/null +++ b/framework/Registry.js @@ -0,0 +1,74 @@ +const Collection = require('@discordjs/collection'); +const fs = require('fs'); +const path = require('path'); +const Group = require('./Group'); + +module.exports = class Registry { + constructor(client) { + Object.defineProperty(this, 'client', { value: client }); + + this.commands = new Collection(); + this.groups = new Collection(); + this.types = new Collection(); + } + + findCommands(query) { + query = query.toLowerCase(); + return this.commands.filter(command => command.name === query || command.aliases.includes(query)); + } + + registerCommand(command) { + this.commands.set(command.name, command); + return this; + } + + registerCommandsIn(dir) { + const groups = fs.readdirSync(dir); + for (const group of groups) { + const commands = fs.readdirSync(path.join(dir, group)); + for (const command of commands) { + if (!command.endsWith('.js')) continue; + const required = require(path.join(dir, group, command)); + this.registerCommand(new required(this.client)); + } + } + return this; + } + + findGroups(query) { + query = query.toLowerCase(); + return this.groups.filter(group => group.id === query || group.name.toLowerCase() === query); + } + + registerGroup(group) { + this.groups.set(group.id, group); + return this; + } + + registerGroups(groups) { + for (const [id, name] of groups) { + const group = new Group(this.client, id, name); + this.registerGroup(group); + } + return this; + } + + registerType(type) { + this.types.set(type.id, type); + return this; + } + + registerTypesIn(dir) { + const types = fs.readdirSync(dir); + for (const type of types) { + if (!type.endsWith('.js')) continue; + const required = require(path.join(dir, type)); + this.registerType(new required(this.client)); + } + return this; + } + + registerDefaultTypes() { + return this.registerTypesIn(path.join(__dirname, 'types')); + } +}; diff --git a/framework/UnionType.js b/framework/UnionType.js new file mode 100644 index 00000000..523b6dda --- /dev/null +++ b/framework/UnionType.js @@ -0,0 +1,37 @@ +const ArgumentType = require('./ArgumentType'); + +module.exports = class ArgumentUnionType extends ArgumentType { + constructor(client, id) { + super(client, id); + + this.types = []; + const typeIDs = id.split('|'); + for (const typeID of typeIDs) { + const type = client.registry.types.get(typeID); + if (!type) throw new Error(`Argument type "${typeID}" is not registered.`); + this.types.push(type); + } + } + + async validate(val, msg, arg) { + let results = this.types.map(type => !type.isEmpty(val, msg, arg) && type.validate(val, msg, arg)); + results = await Promise.all(results); + if (results.some(valid => valid && typeof valid !== 'string')) return true; + const errors = results.filter(valid => typeof valid === 'string'); + if (errors.length > 0) return errors.join('\n'); + return false; + } + + async parse(val, msg, arg) { + let results = this.types.map(type => !type.isEmpty(val, msg, arg) && type.validate(val, msg, arg)); + results = await Promise.all(results); + for (let i = 0; i < results.length; i++) { + if (results[i] && typeof results[i] !== 'string') return this.types[i].parse(val, msg, arg); + } + throw new Error(`Couldn't parse value "${val}" with union type ${this.id}.`); + } + + isEmpty(val, msg, arg) { + return !this.types.some(type => !type.isEmpty(val, msg, arg)); + } +}; diff --git a/framework/types/boolean.js b/framework/types/boolean.js new file mode 100644 index 00000000..f5b5c243 --- /dev/null +++ b/framework/types/boolean.js @@ -0,0 +1,21 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class BooleanArgumentType extends ArgumentType { + constructor(client) { + super(client, 'boolean'); + this.truthy = new Set(['true', 't', 'yes', 'y', 'on', 'enable', 'enabled', '1', '+']); + this.falsy = new Set(['false', 'f', 'no', 'n', 'off', 'disable', 'disabled', '0', '-']); + } + + validate(val) { + const lc = val.toLowerCase(); + return this.truthy.has(lc) || this.falsy.has(lc); + } + + parse(val) { + const lc = val.toLowerCase(); + if (this.truthy.has(lc)) return true; + if (this.falsy.has(lc)) return false; + throw new RangeError('Unknown boolean value.'); + } +} diff --git a/framework/types/channel.js b/framework/types/channel.js new file mode 100644 index 00000000..9ebfbca5 --- /dev/null +++ b/framework/types/channel.js @@ -0,0 +1,39 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class ChannelArgumentType extends ArgumentType { + constructor(client) { + super(client, 'channel'); + } + + validate(val, msg) { + const matches = val.match(/^(?:<#)?([0-9]+)>?$/); + if (matches) return msg.guild.channels.cache.has(matches[1]); + const search = val.toLowerCase(); + const channels = msg.guild.channels.cache.filter(nameFilterInexact(search)); + if (channels.size === 0) return false; + if (channels.size === 1) return true; + const exactChannels = channels.filter(nameFilterExact(search)); + if (exactChannels.size === 1) return true; + return false; + } + + parse(val, msg) { + const matches = val.match(/^(?:<#)?([0-9]+)>?$/); + if (matches) return msg.guild.channels.cache.get(matches[1]) || null; + const search = val.toLowerCase(); + const channels = msg.guild.channels.cache.filter(nameFilterInexact(search)); + if (channels.size === 0) return null; + if (channels.size === 1) return channels.first(); + const exactChannels = channels.filter(nameFilterExact(search)); + if (exactChannels.size === 1) return exactChannels.first(); + return null; + } +} + +function nameFilterExact(search) { + return thing => thing.name.toLowerCase() === search; +} + +function nameFilterInexact(search) { + return thing => thing.name.toLowerCase().includes(search); +} diff --git a/framework/types/command.js b/framework/types/command.js new file mode 100644 index 00000000..23a56271 --- /dev/null +++ b/framework/types/command.js @@ -0,0 +1,17 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class CommandArgumentType extends ArgumentType { + constructor(client) { + super(client, 'command'); + } + + validate(val) { + const commands = this.client.registry.findCommands(val); + if (commands.size === 1) return true; + return false; + } + + parse(val) { + return this.client.registry.findCommands(val).first(); + } +} diff --git a/framework/types/custom-emoji.js b/framework/types/custom-emoji.js new file mode 100644 index 00000000..fd302931 --- /dev/null +++ b/framework/types/custom-emoji.js @@ -0,0 +1,40 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class CustomEmojiArgumentType extends ArgumentType { + constructor(client) { + super(client, 'custom-emoji'); + } + + validate(value, msg) { + const matches = value.match(/^(?:?$/); + if (matches && msg.client.emojis.cache.has(matches[2])) return true; + if (!msg.guild) return false; + const search = value.toLowerCase(); + const emojis = msg.guild.emojis.cache.filter(nameFilterInexact(search)); + if (!emojis.size) return false; + if (emojis.size === 1) return true; + const exactEmojis = emojis.filter(nameFilterExact(search)); + if (exactEmojis.size === 1) return true; + return false; + } + + parse(value, msg) { + const matches = value.match(/^(?:?$/); + if (matches) return msg.client.emojis.cache.get(matches[2]) || null; + const search = value.toLowerCase(); + const emojis = msg.guild.emojis.cache.filter(nameFilterInexact(search)); + if (!emojis.size) return null; + if (emojis.size === 1) return emojis.first(); + const exactEmojis = emojis.filter(nameFilterExact(search)); + if (exactEmojis.size === 1) return exactEmojis.first(); + return null; + } +} + +function nameFilterExact(search) { + return emoji => emoji.name.toLowerCase() === search; +} + +function nameFilterInexact(search) { + return emoji => emoji.name.toLowerCase().includes(search); +} diff --git a/framework/types/default-emoji.js b/framework/types/default-emoji.js new file mode 100644 index 00000000..76c62b7e --- /dev/null +++ b/framework/types/default-emoji.js @@ -0,0 +1,18 @@ +const ArgumentType = require('../ArgumentType'); +const emojiRegex = require('emoji-regex/RGI_Emoji.js'); + +module.exports = class DefaultEmojiArgumentType extends ArgumentType { + constructor(client) { + super(client, 'default-emoji'); + this.regex = new RegExp(`^(?:${emojiRegex().source})$`); + } + + validate(value) { + if (!this.regex.test(value)) return false; + return true; + } + + parse(value) { + return value; + } +} diff --git a/framework/types/float.js b/framework/types/float.js new file mode 100644 index 00000000..e7fa1847 --- /dev/null +++ b/framework/types/float.js @@ -0,0 +1,20 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class FloatArgumentType extends ArgumentType { + constructor(client) { + super(client, 'float'); + } + + validate(val, msg, arg) { + const float = Number.parseFloat(val); + if (Number.isNaN(float)) return false; + if (arg.oneOf && !arg.oneOf.includes(float)) return false; + if (arg.min !== null && typeof arg.min !== 'undefined' && float < arg.min) return false; + if (arg.max !== null && typeof arg.max !== 'undefined' && float > arg.max) return false; + return true; + } + + parse(val) { + return Number.parseFloat(val); + } +} diff --git a/framework/types/group.js b/framework/types/group.js new file mode 100644 index 00000000..74954ea0 --- /dev/null +++ b/framework/types/group.js @@ -0,0 +1,17 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class GroupArgumentType extends ArgumentType { + constructor(client) { + super(client, 'group'); + } + + validate(val) { + const groups = this.client.registry.findGroups(val); + if (groups.size === 1) return true; + return false; + } + + parse(val) { + return this.client.registry.findGroups(val).first(); + } +} diff --git a/framework/types/integer.js b/framework/types/integer.js new file mode 100644 index 00000000..c04187ba --- /dev/null +++ b/framework/types/integer.js @@ -0,0 +1,20 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class IntegerArgumentType extends ArgumentType { + constructor(client) { + super(client, 'integer'); + } + + validate(val, msg, arg) { + const int = Number.parseInt(val); + if (Number.isNaN(int)) return false; + if (arg.oneOf && !arg.oneOf.includes(int)) return false; + if (arg.min !== null && typeof arg.min !== 'undefined' && int < arg.min) return false; + if (arg.max !== null && typeof arg.max !== 'undefined' && int > arg.max) return false; + return true; + } + + parse(val) { + return Number.parseInt(val); + } +} diff --git a/framework/types/member.js b/framework/types/member.js new file mode 100644 index 00000000..031b2d18 --- /dev/null +++ b/framework/types/member.js @@ -0,0 +1,51 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class MemberArgumentType extends ArgumentType { + constructor(client) { + super(client, 'member'); + } + + async validate(val, msg) { + const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/); + if (matches) { + try { + const member = await msg.guild.members.fetch(await this.client.users.fetch(matches[1])); + if (!member) return false; + return true; + } catch (err) { + return false; + } + } + const search = val.toLowerCase(); + const members = msg.guild.members.cache.filter(memberFilterInexact(search)); + if (members.size === 0) return false; + if (members.size === 1) return true; + const exactMembers = members.filter(memberFilterExact(search)); + if (exactMembers.size === 1) return true; + return false; + } + + parse(val, msg) { + const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/); + if (matches) return msg.guild.members.resolve(matches[1]) || null; + const search = val.toLowerCase(); + const members = msg.guild.members.cache.filter(memberFilterInexact(search)); + if (members.size === 0) return null; + if (members.size === 1) return members.first(); + const exactMembers = members.filter(memberFilterExact(search)); + if (exactMembers.size === 1) return exactMembers.first(); + return null; + } +} + +function memberFilterExact(search) { + return mem => mem.user.username.toLowerCase() === search || + (mem.nickname && mem.nickname.toLowerCase() === search) || + mem.tag.toLowerCase() === search; +} + +function memberFilterInexact(search) { + return mem => mem.user.username.toLowerCase().includes(search) || + (mem.nickname && mem.nickname.toLowerCase().includes(search)) || + mem.tag.toLowerCase().includes(search); +} diff --git a/framework/types/message.js b/framework/types/message.js new file mode 100644 index 00000000..6a1305f0 --- /dev/null +++ b/framework/types/message.js @@ -0,0 +1,16 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class MessageArgumentType extends ArgumentType { + constructor(client) { + super(client, 'message'); + } + + async validate(val, msg) { + if (!/^[0-9]+$/.test(val)) return false; + return Boolean(await msg.channel.messages.fetch(val).catch(() => null)); + } + + parse(val, msg) { + return msg.channel.messages.cache.get(val); + } +} diff --git a/framework/types/role.js b/framework/types/role.js new file mode 100644 index 00000000..b1fab753 --- /dev/null +++ b/framework/types/role.js @@ -0,0 +1,39 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class RoleArgumentType extends ArgumentType { + constructor(client) { + super(client, 'role'); + } + + validate(val, msg) { + const matches = val.match(/^(?:<@&)?([0-9]+)>?$/); + if (matches) return msg.guild.roles.cache.has(matches[1]); + const search = val.toLowerCase(); + const roles = msg.guild.roles.cache.filter(nameFilterInexact(search)); + if (roles.size === 0) return false; + if (roles.size === 1) return true; + const exactRoles = roles.filter(nameFilterExact(search)); + if (exactRoles.size === 1) return true; + return false; + } + + parse(val, msg) { + const matches = val.match(/^(?:<@&)?([0-9]+)>?$/); + if (matches) return msg.guild.roles.cache.get(matches[1]) || null; + const search = val.toLowerCase(); + const roles = msg.guild.roles.cache.filter(nameFilterInexact(search)); + if (roles.size === 0) return null; + if (roles.size === 1) return roles.first(); + const exactRoles = roles.filter(nameFilterExact(search)); + if (exactRoles.size === 1) return exactRoles.first(); + return null; + } +} + +function nameFilterExact(search) { + return thing => thing.name.toLowerCase() === search; +} + +function nameFilterInexact(search) { + return thing => thing.name.toLowerCase().includes(search); +} diff --git a/framework/types/string.js b/framework/types/string.js new file mode 100644 index 00000000..73523651 --- /dev/null +++ b/framework/types/string.js @@ -0,0 +1,18 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class StringArgumentType extends ArgumentType { + constructor(client) { + super(client, 'string'); + } + + validate(val, msg, arg) { + if (arg.oneOf && !arg.oneOf.includes(val.toLowerCase())) return false; + if (arg.min !== null && typeof arg.min !== 'undefined' && val.length < arg.min) return false; + if (arg.max !== null && typeof arg.max !== 'undefined' && val.length > arg.max) return false; + return true; + } + + parse(val) { + return val; + } +} diff --git a/framework/types/user.js b/framework/types/user.js new file mode 100644 index 00000000..ea851b8f --- /dev/null +++ b/framework/types/user.js @@ -0,0 +1,53 @@ +const ArgumentType = require('../ArgumentType'); + +module.exports = class UserArgumentType extends ArgumentType { + constructor(client) { + super(client, 'user'); + } + + async validate(val, msg, arg) { + const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/); + if (matches) { + try { + const user = await msg.client.users.fetch(matches[1]); + if (!user) return false; + return true; + } catch (err) { + return false; + } + } + if (!msg.guild) return false; + const search = val.toLowerCase(); + const members = msg.guild.members.cache.filter(memberFilterInexact(search)); + if (members.size === 0) return false; + if (members.size === 1) return true; + const exactMembers = members.filter(memberFilterExact(search)); + if (exactMembers.size === 1) return true; + return false; + } + + parse(val, msg) { + const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/); + if (matches) return msg.client.users.cache.get(matches[1]) || null; + if (!msg.guild) return null; + const search = val.toLowerCase(); + const members = msg.guild.members.cache.filter(memberFilterInexact(search)); + if (members.size === 0) return null; + if (members.size === 1) return members.first().user; + const exactMembers = members.filter(memberFilterExact(search)); + if (exactMembers.size === 1) return exactMembers.first().user; + return null; + } +} + +function memberFilterExact(search) { + return mem => mem.user.username.toLowerCase() === search || + (mem.nickname && mem.nickname.toLowerCase() === search) || + mem.tag.toLowerCase() === search; +} + +function memberFilterInexact(search) { + return mem => mem.user.username.toLowerCase().includes(search) || + (mem.nickname && mem.nickname.toLowerCase().includes(search)) || + mem.tag.toLowerCase().includes(search); +} diff --git a/package.json b/package.json index 7dcfaccd..81d451f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "140.3.1", + "version": "141.0.0", "description": "Your personal server companion.", "main": "Xiao.js", "private": true, @@ -15,12 +15,10 @@ }, "keywords": [ "bot", - "commando", "discord", "discord-api", "discord-bot", - "discord-js", - "discord-js-commando" + "discord-js" ], "author": "dragonfire535 ", "license": "UNLICENSED", @@ -52,7 +50,6 @@ "custom-translate": "^2.2.8", "didyoumean2": "^4.2.0", "discord.js": "github:discordjs/discord.js", - "discord.js-commando": "github:discordjs/Commando", "discord.js-docs": "github:TeeSeal/discord.js-docs", "dotenv": "^9.0.2", "emoji-regex": "^9.2.2", @@ -70,6 +67,7 @@ "kuroshiro": "^1.1.2", "kuroshiro-analyzer-kuromoji": "^1.1.0", "mathjs": "^9.4.0", + "minimist": "^1.2.5", "moment": "^2.29.1", "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.33", diff --git a/structures/Client.js b/structures/Client.js index 6488ddec..ae2c8696 100644 --- a/structures/Client.js +++ b/structures/Client.js @@ -1,4 +1,4 @@ -const { CommandoClient } = require('discord.js-commando'); +const CommandClient = require('../framework/Client'); const { WebhookClient } = require('discord.js'); const request = require('node-superfetch'); const Collection = require('@discordjs/collection'); @@ -12,7 +12,6 @@ const path = require('path'); const Redis = require('./Redis'); const Font = require('./Font'); const BotList = require('./BotList'); -const Patreon = require('./Patreon'); const PhoneManager = require('./phone/PhoneManager'); const TimerManager = require('./remind/TimerManager'); const PokemonStore = require('./pokemon/PokemonStore'); @@ -26,7 +25,7 @@ const { COMMAND_CHANNEL_ID } = process.env; -module.exports = class XiaoClient extends CommandoClient { +module.exports = class XiaoClient extends CommandClient { constructor(options) { super(options); @@ -42,8 +41,6 @@ module.exports = class XiaoClient extends CommandoClient { this.webhook = new WebhookClient(XIAO_WEBHOOK_ID, XIAO_WEBHOOK_TOKEN, { disableMentions: 'everyone' }); this.timers = new TimerManager(this); this.botList = new BotList(this); - this.patreon = new Patreon(); - this.blacklist = { guild: [], user: [] }; this.pokemon = new PokemonStore(); this.games = new Collection(); this.dispatchers = new Map(); @@ -72,110 +69,6 @@ module.exports = class XiaoClient extends CommandoClient { moment.tz.link('America/New_York|Dragon'); } - 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; - } - async fetchAdultSiteList(force = false) { if (!force && this.adultSiteList) return this.adultSiteList; const { text } = await request diff --git a/structures/Command.js b/structures/Command.js deleted file mode 100644 index f26b204e..00000000 --- a/structures/Command.js +++ /dev/null @@ -1,32 +0,0 @@ -const { Command } = require('discord.js-commando'); -const { stripIndents } = require('common-tags'); - -module.exports = class XiaoCommand extends Command { - constructor(client, info) { - if (!info.argsPromptLimit) info.argsPromptLimit = 2; - super(client, info); - - this.patronOnly = info.patronOnly || false; - this.argsSingleQuotes = info.argsSingleQuotes || false; - this.throttling = info.unknown ? null : info.throttling || { usages: 2, duration: 5 }; - this.uses = 0; - this.lastRun = null; - this.credit = info.credit || []; - this.credit.push({ - name: 'Dragon Fire', - url: 'https://github.com/dragonfire535', - reason: 'Code' - }); - } - - hasPermission(msg, ownerOverride) { - if (this.client.isOwner(msg.author)) return true; - if (this.patronOnly && !this.client.patreon.isPatron(msg.author.id)) { - return stripIndents` - The \`${this.name}\` command can only be used by Patrons. - Visit to sign-up! - `; - } - return super.hasPermission(msg, ownerOverride); - } -}; diff --git a/structures/commands/AutoReply.js b/structures/commands/AutoReply.js index 4641a51b..8775d2b1 100644 --- a/structures/commands/AutoReply.js +++ b/structures/commands/AutoReply.js @@ -8,9 +8,9 @@ module.exports = class AutoReplyCommand extends Command { this.throttling = null; } - run(msg, args, fromPattern) { + run(msg) { if (msg.guild && !msg.channel.permissionsFor(this.client.user).has('SEND_MESSAGES')) return null; - const text = this.generateText(fromPattern); + const text = this.generateText(); if (!text) return null; return this.reply ? msg.reply(text) : msg.say(text); } diff --git a/structures/commands/Subreddit.js b/structures/commands/Subreddit.js index bbe5cb63..a2e378ee 100644 --- a/structures/commands/Subreddit.js +++ b/structures/commands/Subreddit.js @@ -18,11 +18,7 @@ module.exports = class SubredditCommand extends Command { }); } - async run(msg, { subreddit }, fromPattern) { - if (fromPattern) { - if (msg.guild && !msg.channel.permissionsFor(this.client.user).has('SEND_MESSAGES')) return null; - subreddit = msg.patternMatches[1]; - } + async run(msg, { subreddit }) { if (!subreddit) subreddit = typeof this.subreddit === 'function' ? this.subreddit() : this.subreddit; try { const post = await this.random(subreddit, msg.channel.nsfw); diff --git a/types/code.js b/types/code.js index 4a2cdc10..6fd68341 100644 --- a/types/code.js +++ b/types/code.js @@ -1,7 +1,7 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); const codeblock = /```(?:(\S+)\n)?\s*([^]+?)\s*```/i; -module.exports = class CodeArgumentType extends ArgumentType { +module.exports = class CodeArgument extends Argument { constructor(client) { super(client, 'code'); } diff --git a/types/font.js b/types/font.js index beb6f4b4..b7bbc316 100644 --- a/types/font.js +++ b/types/font.js @@ -1,14 +1,13 @@ -const { ArgumentType, util: { disambiguation } } = require('discord.js-commando'); -const { escapeMarkdown } = require('discord.js'); +const Argument = require('../framework/ArgumentType'); -module.exports = class FontArgumentType extends ArgumentType { +module.exports = class FontArgument extends Argument { constructor(client) { super(client, 'font'); } validate(value) { const choice = value.toLowerCase(); - let found = this.client.fonts.filter(font => { + const found = this.client.fonts.filter(font => { if (font.isFallback) return false; if (font.name.toLowerCase().includes(choice)) return true; if (font.filenameNoExt.toLowerCase().includes(choice)) return true; @@ -22,10 +21,7 @@ module.exports = class FontArgumentType extends ArgumentType { return false; }); if (foundExact.size === 1) return true; - if (foundExact.size > 0) found = foundExact; - return found.size <= 15 - ? `${disambiguation(found.map(font => escapeMarkdown(font.filenameNoExt)), 'fonts', null)}\n` - : 'Multiple fonts found. Please be more specific.'; + return false; } parse(value) { diff --git a/types/image-or-avatar.js b/types/image-or-avatar.js index c56afa5b..4c79375b 100644 --- a/types/image-or-avatar.js +++ b/types/image-or-avatar.js @@ -1,25 +1,25 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); -module.exports = class ImageOrAvatarArgumentType extends ArgumentType { +module.exports = class ImageOrAvatarArgument extends Argument { constructor(client) { super(client, 'image-or-avatar'); } - async validate(value, msg, arg, currentMsg) { - const image = await this.client.registry.types.get('image').validate(value, msg, arg, currentMsg); + async validate(value, msg, arg) { + const image = await this.client.registry.types.get('image').validate(value, msg, arg); if (image) return image; - return this.client.registry.types.get('user').validate(value, msg, arg, currentMsg); + return this.client.registry.types.get('user').validate(value, msg, arg); } - async parse(value, msg, arg, currentMsg) { - const image = this.client.registry.types.get('image').parse(value, msg, arg, currentMsg); + async parse(value, msg, arg) { + const image = this.client.registry.types.get('image').parse(value, msg, arg); if (image) return image; - const user = await this.client.registry.types.get('user').parse(value, msg, arg, currentMsg); - return user.displayAvatarURL({ format: 'png', size: 512 }); + const user = await this.client.registry.types.get('user').parse(value, msg, arg); + return user.displayAvatarURL({ format: 'png', size: arg.avatarSize || 512 }); } - isEmpty(value, msg, arg, currentMsg) { - return this.client.registry.types.get('image').isEmpty(value, msg, arg, currentMsg) - && this.client.registry.types.get('user').isEmpty(value, msg, arg, currentMsg); + isEmpty(value, msg, arg) { + return this.client.registry.types.get('image').isEmpty(value, msg, arg) + && this.client.registry.types.get('user').isEmpty(value, msg, arg); } }; diff --git a/types/image.js b/types/image.js index 60b3b6c7..ab1c844f 100644 --- a/types/image.js +++ b/types/image.js @@ -1,18 +1,18 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); const fileTypeRe = /\.(jpe?g|png|gif|jfif|bmp)(\?.+)?$/i; const request = require('node-superfetch'); const validURL = require('valid-url'); -module.exports = class ImageArgumentType extends ArgumentType { +module.exports = class ImageArgument extends Argument { constructor(client) { super(client, 'image'); } - async validate(value, msg, arg, currentMsg) { - const attachment = currentMsg.attachments.first(); + async validate(value, msg) { + const attachment = msg.attachments.first(); if (attachment) { - if (attachment.size > 8e+6) return 'Please provide an image under 8 MB.'; - if (!fileTypeRe.test(attachment.name)) return 'Please only send PNG, JPG, BMP, or GIF format images.'; + if (attachment.size > 8e+6) return false; + if (!fileTypeRe.test(attachment.name)) return false; return true; } if (fileTypeRe.test(value.toLowerCase())) { @@ -27,15 +27,15 @@ module.exports = class ImageArgumentType extends ArgumentType { return false; } - parse(value, msg, arg, currentMsg) { - const attachment = currentMsg.attachments.first(); + parse(value, msg) { + const attachment = msg.attachments.first(); if (attachment) return attachment.url; if (fileTypeRe.test(value.toLowerCase())) return value; return null; } - isEmpty(value, msg, arg, currentMsg) { - if (currentMsg.attachments.size) return false; + isEmpty(value, msg) { + if (msg.attachments.size) return false; return !value; } }; diff --git a/types/month.js b/types/month.js index 1f2943c7..8fe00ab4 100644 --- a/types/month.js +++ b/types/month.js @@ -1,7 +1,7 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); const { months, shorthand } = require('../assets/json/month'); -module.exports = class MonthArgumentType extends ArgumentType { +module.exports = class MonthArgument extends Argument { constructor(client) { super(client, 'month'); } diff --git a/types/pokemon.js b/types/pokemon.js index 67b3cd57..e04bfdd4 100644 --- a/types/pokemon.js +++ b/types/pokemon.js @@ -1,6 +1,6 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); -module.exports = class PokemonArgumentType extends ArgumentType { +module.exports = class PokemonArgument extends Argument { constructor(client) { super(client, 'pokemon'); } diff --git a/types/sherlock.js b/types/sherlock.js index 618ad702..4cd7c082 100644 --- a/types/sherlock.js +++ b/types/sherlock.js @@ -1,14 +1,14 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); const sherlock = require('sherlockjs'); -module.exports = class SherlockType extends ArgumentType { +module.exports = class SherlockType extends Argument { constructor(client) { super(client, 'sherlock'); } validate(value) { const time = sherlock.parse(value); - if (!time.startDate) return 'Please provide a valid starting time.'; + if (!time.startDate) return false; return true; } diff --git a/types/timezone.js b/types/timezone.js index 49484427..99637fbb 100644 --- a/types/timezone.js +++ b/types/timezone.js @@ -1,9 +1,9 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); const cityTimezones = require('city-timezones'); const { ZipToTz } = require('zip-to-timezone'); const moment = require('moment-timezone'); -module.exports = class TimezoneType extends ArgumentType { +module.exports = class TimezoneType extends Argument { constructor(client) { super(client, 'timezone'); } diff --git a/types/url.js b/types/url.js index 0a593ea9..a3dc46ae 100644 --- a/types/url.js +++ b/types/url.js @@ -1,8 +1,8 @@ -const { ArgumentType } = require('discord.js-commando'); +const Argument = require('../framework/ArgumentType'); const { URL } = require('url'); const validURL = require('valid-url'); -module.exports = class UrlType extends ArgumentType { +module.exports = class UrlType extends Argument { constructor(client) { super(client, 'url'); } diff --git a/util/Util.js b/util/Util.js index 401c9230..9ee8a3be 100644 --- a/util/Util.js +++ b/util/Util.js @@ -17,6 +17,10 @@ module.exports = class Util { return new Promise(resolve => setTimeout(resolve, ms)); } + static escapeRegex(str) { + return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); + } + static shuffle(array) { const arr = array.slice(0); for (let i = arr.length - 1; i >= 0; i--) {