From 5ca346727aff5cc6050c3a593d2e84a56d43b6dc Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 22 May 2020 16:43:49 -0500 Subject: [PATCH] v1.0.0-alpha (#696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial teams support (#692) * initial teams support * update typings * whoops * whoops v2 * Add Monitor#allowedTypes (#695) Limits messages based on the Message#type * remove client from Piece constructors (#689) * remove client from Piece constructors * typings * Add Monitor#allowedTypes (#695) Limits messages based on the Message#type * Text Prompt Rework (#682) * Text prompt arbitrary user channel (#681) * [wip] maybe * fix target mention * oh right, bots message not theirs * typings: Update for latest commit * fix bad rebase * typings: Fixed missing change in rebase * typings: Remove all instances of GroupDMChannel * Abort the prompt if the user retypes the command (#683) * Abort if command retyped The way this is done properly is to move much of CommandHandler to internals so there isn't a race between monitor/prompter. * fix since docs * fix doc type * fix some order errors * fix typo * minimize impact if original command is edited * typings * make guildSettings not a getter once again * fix crash if you get a message before klasaReady * more optimal crash protection * allow flagSupport to be turned off * seperate argumentError from commandError (#694) * rename event files that have core klasa functionality (#688) * fix bad merge? * fix mentioned bug * add a retries loop for when discord is tripping over cords * typos * fix dm prompt * fix bad error * remove duplicate identifier (#699) * fix(KlasaMessage): reactable not checking READ_MESSAGE_HISTORY permissions * Update Possible.js * typings: Add mentionPrefix on the client * Update Tag.js * ScheduledTask#catchUp is boolean (#712) * src: Fix Store reloading broadcast script * docs: Correct Piece file description It's to the piece's file, not extendables * src: Fix Language creation for loading core languages * typings: Mark Language#store as a LanguageStore * revert(typings): Revert marking of Language#store as LanguageStore * add code for proper teams support dependent on d.js imp (#732) * Update Store.js * add null to usageDelim typings (#750) * add null to usageDelim typings * set eval usage to null to better support pieceDefaults * Update index.d.ts (#744) * Update index.d.ts * Update typings/index.d.ts Co-Authored-By: Antonio Román * require necessary addditions * update jsdocs * Fix cooldown response (#743) * Update cooldown.js * Update en-US.js * Update en-US.js * Update src/inhibitors/cooldown.js Co-Authored-By: Antonio Román * Fix(Typing): Add category and subCategory to PieceCommandJSON (#759) * Chore(deps): Bump lodash from 4.17.11 to 4.17.14 (#749) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14) Signed-off-by: dependabot[bot] * Add category and subCategory to PieceCommandJSON * fix: Resolved memory leak in Schedule Co-Authored-By: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Thanks Gryffon for finding the bug! * Fix no named types (#741) * Fix no named types * Update src/lib/util/Type.js Co-Authored-By: Hutch * fix lint * Remove old method (#767) * Fix finalizerError typings (#766) * Fix(Typing): Add string union for EventOptions#emitter (#764) * Fix(Typing): Add string union for EventOptions#emitter * Overcomplicate cause why not :p * remove private message#levelID the command in the inhibitor/finalizer is not necessarily the message.command if for instance it is a reaction command. Therefore, that command has to be taken account for, not the one potentually on message, making levelID useless. Since it's private nobody should be using it, therefore it's not breaking to remove it. * bye bye Command#cooldowns inb4 crying that other pieces don't have types so this is just as bad or worse than "privates in pieces" closes #770 * src: Replace client.shard.id with client.options.shards (#772) * src: Replace client.shard.id with client.options.shards * src: Kyra notice * FIx(Typings): Forgot to import Stopwatch (#773) * Fix commands being re-run on non-command edits * Fix a typo (#776) * Client.use should use the util class (#784) * Cooldown Inhibitor Change Seconds To Formatted String (#763) * Update cooldown.js * Update en-US.js * Update Duration.js * Update cooldown.js * Update cooldown.js * Update Duration.js * Update cooldown.js * Update Duration.js * Update cooldown.js * Update cooldown.js * Update cooldown.js * Update cooldown.js * Update cooldown.js * Update cooldown.js * Update cooldown.js * perf(Timestamp): Increased performance and move patterns to maps (#762) * perf: Avoid megamorphic ICs and increased performance Now everything is monomorphic and can run fully inlined, yay! * docs: Documented Timestamp's static properties * docs: We should switch to TSDocs... * docs: Fixed type and renamed property * Timestamp: Remove `tokenRepeatingCounts` The property has been 👢'd! * Timestamp: Inline all functions * Timestamp: Resolve has+get anti-pattern * Constants: Make TIME.TIMESTAMP.TOKENS a map And removed `tokenRepeatingCounts` from Timestamp to reduce memory footprint * Typings: Updated signature for `ConstantsTime.TIMESTAMP.TOKENS` * fix: Y and YY formats slicing the first two digits instead of the two last If it's 2019, it should output 19, not 20. * typings: include klasaReady (#796) * typings: Added flagSupport to TextPromptOptions * typings: Add overrides from discord.js to klasa send methods (#794) * Revert "Cooldown Inhibitor Change Seconds To Formatted String (#763)" (#835) This reverts commit 2ff196b94af0271f7f2dee9425d98d6ad9abe5b2. * mention prefix should be checked first so a prefix of < doesn't break mention prefixes * Fix(Commands): renamed event and unified end behavior of commands (#865) * Update disable.js * Update enable.js * client.guilds.cache.size (#963) client.guilds.size is undefined in d.js now * d.js managers fix for resolveGuild (#956) Co-authored-by: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Co-authored-by: Antonio Román Co-authored-by: Vlad Frangu Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Co-authored-by: Jacz <23615291+MrJacz@users.noreply.github.com> Co-authored-by: Kolkies Co-authored-by: Jeroen Claassens Co-authored-by: Jacz Co-authored-by: Gryffon Bellish Co-authored-by: Wingysam Co-authored-by: Jonathan Ford --- guides/Getting Started/GettingStarted.md | 2 +- .../UnderstandingPermissionLevels.md | 8 +- src/commands/Admin/disable.js | 4 +- src/commands/Admin/enable.js | 4 +- src/commands/Admin/eval.js | 3 +- src/commands/Admin/load.js | 2 +- src/commands/Admin/reload.js | 6 +- src/commands/Admin/transfer.js | 2 +- src/commands/Admin/unload.js | 2 +- src/events/argumentError.js | 9 + .../{guildCreate.js => coreGuildCreate.js} | 4 + .../{guildDelete.js => coreGuildDelete.js} | 4 + src/events/{message.js => coreMessage.js} | 4 + ...{messageDelete.js => coreMessageDelete.js} | 4 + ...DeleteBulk.js => coreMessageDeleteBulk.js} | 4 + ...{messageUpdate.js => coreMessageUpdate.js} | 4 + ...ateEntry.js => coreSettingsUpdateEntry.js} | 6 +- src/events/onceReady.js | 21 +- src/finalizers/commandCooldown.js | 20 +- src/inhibitors/cooldown.js | 12 +- src/inhibitors/hidden.js | 2 +- src/inhibitors/slowmode.js | 2 +- src/languages/en-US.js | 3 +- src/lib/Client.js | 42 +++- src/lib/extensions/KlasaMessage.js | 184 +++++++++++----- src/lib/schedule/Schedule.js | 2 +- src/lib/schedule/ScheduledTask.js | 5 +- src/lib/structures/Command.js | 49 ++--- src/lib/structures/Event.js | 5 +- src/lib/structures/Extendable.js | 5 +- src/lib/structures/Inhibitor.js | 5 +- src/lib/structures/Language.js | 2 +- src/lib/structures/Monitor.js | 5 +- src/lib/structures/base/AliasPiece.js | 5 +- src/lib/structures/base/Piece.js | 9 +- src/lib/structures/base/Store.js | 4 +- src/lib/usage/CommandUsage.js | 2 +- src/lib/usage/Possible.js | 2 +- src/lib/usage/Tag.js | 2 +- src/lib/usage/TextPrompt.js | 58 ++++- src/lib/util/Timestamp.js | 195 ++++++++--------- src/lib/util/Type.js | 2 +- src/lib/util/constants.js | 49 ++--- src/lib/util/util.js | 2 +- src/monitors/commandHandler.js | 83 ++------ typings/index.d.ts | 201 +++++++++--------- 46 files changed, 588 insertions(+), 462 deletions(-) create mode 100644 src/events/argumentError.js rename src/events/{guildCreate.js => coreGuildCreate.js} (80%) rename src/events/{guildDelete.js => coreGuildDelete.js} (75%) rename src/events/{message.js => coreMessage.js} (70%) rename src/events/{messageDelete.js => coreMessageDelete.js} (75%) rename src/events/{messageDeleteBulk.js => coreMessageDeleteBulk.js} (78%) rename src/events/{messageUpdate.js => coreMessageUpdate.js} (75%) rename src/events/{settingsUpdateEntry.js => coreSettingsUpdateEntry.js} (76%) diff --git a/guides/Getting Started/GettingStarted.md b/guides/Getting Started/GettingStarted.md index 9f31972279..9f351064c1 100644 --- a/guides/Getting Started/GettingStarted.md +++ b/guides/Getting Started/GettingStarted.md @@ -20,7 +20,7 @@ new Client({ prefix: '+', commandEditing: true, typing: true, - readyMessage: (client) => `Successfully initialized. Ready to serve ${client.guilds.size} guilds.` + readyMessage: (client) => `Successfully initialized. Ready to serve ${client.guilds.cache.size} guilds.` }).login('your-bot-token'); ``` diff --git a/guides/Getting Started/UnderstandingPermissionLevels.md b/guides/Getting Started/UnderstandingPermissionLevels.md index 9f944c97d5..703ddcad1c 100644 --- a/guides/Getting Started/UnderstandingPermissionLevels.md +++ b/guides/Getting Started/UnderstandingPermissionLevels.md @@ -31,10 +31,10 @@ function permissionLevel(message) { if (message.guild && message.member === message.guild.owner) return true; case 8: case 9: - if (message.author === message.client.owner) return true; + if (message.client.owners.has(author)) return true; break; case 10: - if (message.author === message.client.owner) return true; + if (message.client.owners.has(author)) return true; return false; } throw 'You don\'t have permission'; @@ -74,9 +74,9 @@ config.permissionLevels = new PermissionLevels() * Allows the Bot Owner to use any lower commands * and causes any command with a permission level 9 or lower to return an error if no check passes. */ - .add(9, ({ author, client }) => author === client.owner, { break: true }) + .add(9, ({ author, client }) => client.owners.has(author), { break: true }) // Allows the bot owner to use Bot Owner only commands, which silently fail for other users. - .add(10, ({ author, client }) => author === client.owner); + .add(10, ({ author, client }) => client.owners.has(author)); new Client(config).login(config.token); ``` diff --git a/src/commands/Admin/disable.js b/src/commands/Admin/disable.js index 299eef3bec..052da470d1 100644 --- a/src/commands/Admin/disable.js +++ b/src/commands/Admin/disable.js @@ -12,13 +12,13 @@ module.exports = class extends Command { } async run(message, [piece]) { - if ((piece.type === 'event' && piece.name === 'message') || (piece.type === 'monitor' && piece.name === 'commandHandler')) { + if ((piece.type === 'event' && piece.name === 'coreMessage') || (piece.type === 'monitor' && piece.name === 'commandHandler')) { return message.sendLocale('COMMAND_DISABLE_WARN'); } piece.disable(); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.${piece.store}.get('${piece.name}').disable(); + if (String(this.options.shards) !== '${this.client.options.shards}') this.${piece.store}.get('${piece.name}').disable(); `); } return message.sendLocale('COMMAND_DISABLE', [piece.type, piece.name], { code: 'diff' }); diff --git a/src/commands/Admin/enable.js b/src/commands/Admin/enable.js index 97dee3f5e2..b772129119 100644 --- a/src/commands/Admin/enable.js +++ b/src/commands/Admin/enable.js @@ -15,10 +15,10 @@ module.exports = class extends Command { piece.enable(); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.${piece.store}.get('${piece.name}').enable(); + if (String(this.options.shards) !== '${this.client.options.shards}') this.${piece.store}.get('${piece.name}').enable(); `); } - return message.sendCode('diff', message.language.get('COMMAND_ENABLE', piece.type, piece.name)); + return message.sendLocale('COMMAND_ENABLE', [piece.type, piece.name], { code: 'diff' }); } }; diff --git a/src/commands/Admin/eval.js b/src/commands/Admin/eval.js index 68798e5d26..103d86c4c6 100644 --- a/src/commands/Admin/eval.js +++ b/src/commands/Admin/eval.js @@ -10,7 +10,8 @@ module.exports = class extends Command { guarded: true, description: language => language.get('COMMAND_EVAL_DESCRIPTION'), extendedHelp: language => language.get('COMMAND_EVAL_EXTENDEDHELP'), - usage: '' + usage: '', + usageDelim: null }); } diff --git a/src/commands/Admin/load.js b/src/commands/Admin/load.js index 39f5a68708..4ab6d3b4bb 100644 --- a/src/commands/Admin/load.js +++ b/src/commands/Admin/load.js @@ -26,7 +26,7 @@ module.exports = class extends Command { await piece.init(); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') { + if (String(this.options.shards) !== '${this.client.options.shards}') { const piece = this.${piece.store}.load('${piece.directory}', ${JSON.stringify(path)}); if (piece) piece.init(); } diff --git a/src/commands/Admin/reload.js b/src/commands/Admin/reload.js index f47592dca4..a23e36401e 100644 --- a/src/commands/Admin/reload.js +++ b/src/commands/Admin/reload.js @@ -20,7 +20,7 @@ module.exports = class extends Command { await piece.init(); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.${piece.name}.loadAll().then(() => this.${piece.name}.loadAll()); + if (String(this.options.shards) !== '${this.client.options.shards}') this.${piece.name}.loadAll().then(() => this.${piece.name}.init()); `); } return message.sendLocale('COMMAND_RELOAD_ALL', [piece, timer.stop()]); @@ -31,7 +31,7 @@ module.exports = class extends Command { const timer = new Stopwatch(); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.${piece.store}.get('${piece.name}').reload(); + if (String(this.options.shards) !== '${this.client.options.shards}') this.${piece.store}.get('${piece.name}').reload(); `); } return message.sendLocale('COMMAND_RELOAD', [itm.type, itm.name, timer.stop()]); @@ -49,7 +49,7 @@ module.exports = class extends Command { })); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.pieceStores.map(async (store) => { + if (String(this.options.shards) !== '${this.client.options.shards}') this.pieceStores.map(async (store) => { await store.loadAll(); await store.init(); }); diff --git a/src/commands/Admin/transfer.js b/src/commands/Admin/transfer.js index 85767b595b..c34d11764e 100644 --- a/src/commands/Admin/transfer.js +++ b/src/commands/Admin/transfer.js @@ -22,7 +22,7 @@ module.exports = class extends Command { piece.store.load(piece.store.userDirectory, piece.file); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.${piece.store}.load(${piece.store.userDirectory}, ${JSON.stringify(piece.file)}); + if (String(this.options.shards) !== '${this.client.options.shards}') this.${piece.store}.load(${piece.store.userDirectory}, ${JSON.stringify(piece.file)}); `); } return message.sendLocale('COMMAND_TRANSFER_SUCCESS', [piece.type, piece.name]); diff --git a/src/commands/Admin/unload.js b/src/commands/Admin/unload.js index 51f97cc1ab..3ee94c2a8a 100644 --- a/src/commands/Admin/unload.js +++ b/src/commands/Admin/unload.js @@ -19,7 +19,7 @@ module.exports = class extends Command { piece.unload(); if (this.client.shard) { await this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') this.${piece.store}.get('${piece.name}').unload(); + if (String(this.options.shards) !== '${this.client.options.shards}') this.${piece.store}.get('${piece.name}').unload(); `); } return message.sendLocale('COMMAND_UNLOAD', [piece.type, piece.name]); diff --git a/src/events/argumentError.js b/src/events/argumentError.js new file mode 100644 index 0000000000..b68fad9258 --- /dev/null +++ b/src/events/argumentError.js @@ -0,0 +1,9 @@ +const { Event } = require('klasa'); + +module.exports = class extends Event { + + run(message, command, params, error) { + message.sendMessage(error).catch(err => this.client.emit('wtf', err)); + } + +}; diff --git a/src/events/guildCreate.js b/src/events/coreGuildCreate.js similarity index 80% rename from src/events/guildCreate.js rename to src/events/coreGuildCreate.js index e4d5a1572e..23b57e958c 100644 --- a/src/events/guildCreate.js +++ b/src/events/coreGuildCreate.js @@ -2,6 +2,10 @@ const { Event } = require('klasa'); module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'guildCreate' }); + } + run(guild) { if (!guild.available) return; if (this.client.settings.guildBlacklist.includes(guild.id)) { diff --git a/src/events/guildDelete.js b/src/events/coreGuildDelete.js similarity index 75% rename from src/events/guildDelete.js rename to src/events/coreGuildDelete.js index 251e3cc367..ca0325ab47 100644 --- a/src/events/guildDelete.js +++ b/src/events/coreGuildDelete.js @@ -2,6 +2,10 @@ const { Event } = require('klasa'); module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'guildDelete' }); + } + run(guild) { if (this.client.ready && guild.available && !this.client.options.preserveSettings) guild.settings.destroy().catch(() => null); } diff --git a/src/events/message.js b/src/events/coreMessage.js similarity index 70% rename from src/events/message.js rename to src/events/coreMessage.js index d490e80b1a..2e78e3bd5e 100644 --- a/src/events/message.js +++ b/src/events/coreMessage.js @@ -2,6 +2,10 @@ const { Event } = require('klasa'); module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'message' }); + } + run(message) { if (this.client.ready) this.client.monitors.run(message); } diff --git a/src/events/messageDelete.js b/src/events/coreMessageDelete.js similarity index 75% rename from src/events/messageDelete.js rename to src/events/coreMessageDelete.js index 6bad3e1501..fc29650be4 100644 --- a/src/events/messageDelete.js +++ b/src/events/coreMessageDelete.js @@ -2,6 +2,10 @@ const { Event } = require('klasa'); module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'messageDelete' }); + } + run(message) { if (message.command && message.command.deletable) { for (const msg of message.responses) { diff --git a/src/events/messageDeleteBulk.js b/src/events/coreMessageDeleteBulk.js similarity index 78% rename from src/events/messageDeleteBulk.js rename to src/events/coreMessageDeleteBulk.js index 4009b5eeaa..c0980601a0 100644 --- a/src/events/messageDeleteBulk.js +++ b/src/events/coreMessageDeleteBulk.js @@ -2,6 +2,10 @@ const { Event } = require('klasa'); module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'messageDeleteBulk' }); + } + run(messages) { for (const message of messages.values()) { if (message.command && message.command.deletable) { diff --git a/src/events/messageUpdate.js b/src/events/coreMessageUpdate.js similarity index 75% rename from src/events/messageUpdate.js rename to src/events/coreMessageUpdate.js index 62b6ee7c5e..3dc3cb9c1e 100644 --- a/src/events/messageUpdate.js +++ b/src/events/coreMessageUpdate.js @@ -2,6 +2,10 @@ const { Event } = require('klasa'); module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'messageUpdate' }); + } + async run(old, message) { if (this.client.ready && !old.partial && old.content !== message.content) this.client.monitors.run(message); } diff --git a/src/events/settingsUpdateEntry.js b/src/events/coreSettingsUpdateEntry.js similarity index 76% rename from src/events/settingsUpdateEntry.js rename to src/events/coreSettingsUpdateEntry.js index 76c547de62..09fb619f47 100644 --- a/src/events/settingsUpdateEntry.js +++ b/src/events/coreSettingsUpdateEntry.js @@ -3,10 +3,14 @@ const gateways = ['users', 'clientStorage']; module.exports = class extends Event { + constructor(...args) { + super(...args, { event: 'settingsUpdateEntry' }); + } + run(settings) { if (gateways.includes(settings.gateway.type)) { this.client.shard.broadcastEval(` - if (String(this.shard.id) !== '${this.client.shard.id}') { + if (String(this.options.shards) !== '${this.client.options.shards}') { const entry = this.gateways.${settings.gateway.type}.get('${settings.id}'); if (entry) { entry._patch(${JSON.stringify(settings)}); diff --git a/src/events/onceReady.js b/src/events/onceReady.js index 10ad4e6316..7691dc22a6 100644 --- a/src/events/onceReady.js +++ b/src/events/onceReady.js @@ -1,4 +1,6 @@ const { Event, util } = require('klasa'); +const { Team } = require('discord.js'); +let retries = 0; module.exports = class extends Event { @@ -10,8 +12,21 @@ module.exports = class extends Event { } async run() { - await this.client.fetchApplication(); - if (!this.client.options.ownerID) this.client.options.ownerID = this.client.application.owner.id; + try { + await this.client.fetchApplication(); + } catch (err) { + if (++retries === 3) return process.exit(); + this.client.emit('warning', `Unable to fetchApplication at this time, waiting 5 seconds and retrying. Retries left: ${retries - 3}`); + await util.sleep(5000); + return this.run(); + } + + if (!this.client.options.owners.length) { + if (this.client.application.owner instanceof Team) this.client.options.owners.push(...this.client.application.owner.members.keys()); + else this.client.options.owners.push(this.client.application.owner.id); + } + + this.client.mentionPrefix = new RegExp(`^<@!?${this.client.user.id}>`); this.client.settings = this.client.gateways.clientStorage.get(this.client.user.id, true); // Added for consistency with other datastores, Client#clients does not exist @@ -30,7 +45,7 @@ module.exports = class extends Event { this.client.emit('log', util.isFunction(this.client.options.readyMessage) ? this.client.options.readyMessage(this.client) : this.client.options.readyMessage); } - this.client.emit('klasaReady'); + return this.client.emit('klasaReady'); } }; diff --git a/src/finalizers/commandCooldown.js b/src/finalizers/commandCooldown.js index 8f09e4193b..98d9ddf405 100644 --- a/src/finalizers/commandCooldown.js +++ b/src/finalizers/commandCooldown.js @@ -1,15 +1,29 @@ -const { Finalizer } = require('klasa'); +const { Finalizer, RateLimitManager } = require('klasa'); module.exports = class extends Finalizer { + constructor(...args) { + super(...args); + this.cooldowns = new WeakMap(); + } + run(message, command) { - if (command.cooldown <= 0 || message.author === this.client.owner) return; + if (command.cooldown <= 0 || this.client.owners.has(message.author)) return; try { - command.cooldowns.acquire(message.levelID).drip(); + this.getCooldown(message, command).drip(); } catch (err) { this.client.emit('error', `${message.author.username}[${message.author.id}] has exceeded the RateLimit for ${message.command}`); } } + getCooldown(message, command) { + let cooldownManager = this.cooldowns.get(command); + if (!cooldownManager) { + cooldownManager = new RateLimitManager(command.bucket, command.cooldown * 1000); + this.cooldowns.set(command, cooldownManager); + } + return cooldownManager.acquire(message.guild ? message[command.cooldownLevel].id : message.author.id); + } + }; diff --git a/src/inhibitors/cooldown.js b/src/inhibitors/cooldown.js index 59d53fd261..3c2b3acd76 100644 --- a/src/inhibitors/cooldown.js +++ b/src/inhibitors/cooldown.js @@ -7,11 +7,17 @@ module.exports = class extends Inhibitor { } run(message, command) { - if (message.author === this.client.owner || command.cooldown <= 0) return; + if (this.client.owners.has(message.author) || command.cooldown <= 0) return; - const existing = command.cooldowns.get(message.levelID); + let existing; - if (existing && existing.limited) throw message.language.get('INHIBITOR_COOLDOWN', Math.ceil(existing.remainingTime / 1000)); + try { + existing = this.client.finalizers.get('commandCooldown').getCooldown(message, command); + } catch (err) { + return; + } + + if (existing && existing.limited) throw message.language.get('INHIBITOR_COOLDOWN', Math.ceil(existing.remainingTime / 1000), command.cooldownLevel !== 'author'); } }; diff --git a/src/inhibitors/hidden.js b/src/inhibitors/hidden.js index c377feceb5..ed4e99ad33 100644 --- a/src/inhibitors/hidden.js +++ b/src/inhibitors/hidden.js @@ -3,7 +3,7 @@ const { Inhibitor } = require('klasa'); module.exports = class extends Inhibitor { run(message, command) { - return command.hidden && message.command !== command && message.author !== this.client.owner; + return command.hidden && message.command !== command && !this.client.owners.has(message.author); } }; diff --git a/src/inhibitors/slowmode.js b/src/inhibitors/slowmode.js index ec75ed354c..640b5252f9 100644 --- a/src/inhibitors/slowmode.js +++ b/src/inhibitors/slowmode.js @@ -11,7 +11,7 @@ module.exports = class extends Inhibitor { } run(message) { - if (message.author === this.client.owner) return; + if (this.client.owners.has(message.author)) return; const rateLimit = this.slowmode.acquire(message.author.id); diff --git a/src/languages/en-US.js b/src/languages/en-US.js index 271f9c0855..950cfaa0ad 100644 --- a/src/languages/en-US.js +++ b/src/languages/en-US.js @@ -54,7 +54,8 @@ module.exports = class extends Language { // eslint-disable-next-line max-len MONITOR_COMMAND_HANDLER_REPEATING_REPROMPT: (tag, name, time, cancelOptions) => `${tag} | **${name}** is a repeating argument | You have **${time}** seconds to respond to this prompt with additional valid arguments. Type **${cancelOptions.join('**, **')}** to cancel this prompt.`, MONITOR_COMMAND_HANDLER_ABORTED: 'Aborted', - INHIBITOR_COOLDOWN: (remaining) => `You have just used this command. You can use this command again in ${remaining} second${remaining === 1 ? '' : 's'}.`, + // eslint-disable-next-line max-len + INHIBITOR_COOLDOWN: (remaining, guildCooldown) => `${guildCooldown ? 'Someone has' : 'You have'} already used this command. You can use this command again in ${remaining} second${remaining === 1 ? '' : 's'}.`, INHIBITOR_DISABLED_GUILD: 'This command has been disabled by an admin in this guild.', INHIBITOR_DISABLED_GLOBAL: 'This command has been globally disabled by the bot owner.', INHIBITOR_MISSING_BOT_PERMS: (missing) => `Insufficient permissions, missing: **${missing}**`, diff --git a/src/lib/Client.js b/src/lib/Client.js index e097bb7a62..049bdcf2bb 100644 --- a/src/lib/Client.js +++ b/src/lib/Client.js @@ -66,7 +66,7 @@ class KlasaClient extends Discord.Client { * @property {GatewaysOptions} [gateways={}] The options for each built-in gateway * @property {string} [language='en-US'] The default language Klasa should opt-in for the commands * @property {boolean} [noPrefixDM=false] Whether the bot should allow prefixless messages in DMs - * @property {string} [ownerID] The discord user id for the user the bot should respect as the owner (gotten from Discord api if not provided) + * @property {string[]} [owners] The discord user id for the users the bot should respect as the owner (gotten from Discord api if not provided) * @property {PermissionLevelsOverload} [permissionLevels] The permission levels to use with this bot * @property {PieceDefaults} [pieceDefaults={}] Overrides the defaults for all pieces * @property {string|string[]} [prefix] The default prefix the bot should respond to @@ -324,6 +324,13 @@ class KlasaClient extends Discord.Client { */ this.ready = false; + /** + * The regexp for a prefix mention + * @since 0.5.0 + * @type {RegExp} + */ + this.mentionPrefix = null; + // Run all plugin functions in this context for (const plugin of plugins) plugin.call(this); } @@ -340,13 +347,18 @@ class KlasaClient extends Discord.Client { } /** - * The owner for this bot - * @since 0.1.1 - * @type {?KlasaUser} + * The owners for this bot + * @since 0.5.0 + * @type {Set} * @readonly */ - get owner() { - return this.users.cache.get(this.options.ownerID) || null; + get owners() { + const owners = new Set(); + for (const owner of this.options.owners) { + const user = this.users.cache.get(owner); + if (user) owners.add(user); + } + return owners; } /** @@ -469,7 +481,7 @@ class KlasaClient extends Discord.Client { */ static use(mod) { const plugin = mod[this.plugin]; - if (typeof plugin !== 'function') throw new TypeError('The provided module does not include a plugin function'); + if (!util.isFunction(plugin)) throw new TypeError('The provided module does not include a plugin function'); plugins.add(plugin); return this; } @@ -501,8 +513,8 @@ KlasaClient.defaultPermissionLevels = new PermissionLevels() .add(0, () => true) .add(6, ({ guild, member }) => guild && member.permissions.has(FLAGS.MANAGE_GUILD), { fetch: true }) .add(7, ({ guild, member }) => guild && member === guild.owner, { fetch: true }) - .add(9, ({ author, client }) => author === client.owner, { break: true }) - .add(10, ({ author, client }) => author === client.owner); + .add(9, ({ author, client }) => client.owners.has(author), { break: true }) + .add(10, ({ author, client }) => client.owners.has(author)); /** @@ -610,7 +622,17 @@ KlasaClient.defaultClientSchema = new Schema() * @param {KlasaMessage} message The message that triggered the command * @param {Command} command The command run * @param {any[]} params The resolved parameters of the command - * @param {(string|Object)} error The command error + * @param {Object} error The command error + */ + +/** + * Emitted when an invalid argument is passed to a command. + * @event KlasaClient#argumentError + * @since 0.5.0 + * @param {KlasaMessage} message The message that triggered the command + * @param {Command} command The command run + * @param {any[]} params The resolved parameters of the command + * @param {string} error The argument error */ /** diff --git a/src/lib/extensions/KlasaMessage.js b/src/lib/extensions/KlasaMessage.js index b289824e05..7d6cde272d 100644 --- a/src/lib/extensions/KlasaMessage.js +++ b/src/lib/extensions/KlasaMessage.js @@ -1,4 +1,5 @@ const { Structures, Collection, APIMessage, Permissions: { FLAGS } } = require('discord.js'); +const { regExpEsc } = require('../util/util'); module.exports = Structures.extend('Message', Message => { /** @@ -7,39 +8,45 @@ module.exports = Structures.extend('Message', Message => { */ class KlasaMessage extends Message { + /** + * @typedef {object} CachedPrefix + * @property {number} length The length of the prefix + * @property {RegExp | null} regex The RegExp for the prefix + */ + /** * @param {...*} args Normal D.JS Message args */ constructor(...args) { super(...args); - /** - * The guild level settings for this context (guild || default) - * @since 0.5.0 - * @type {Settings} - */ - this.guildSettings = this.guild ? this.guild.settings : this.client.gateways.guilds.defaults; - /** * The command being run * @since 0.0.1 * @type {?Command} */ - this.command = null; + this.command = this.command || null; + + /** + * The name of the command being run + * @since 0.5.0 + * @type {?string} + */ + this.commandText = this.commandText || null; /** * The prefix used * @since 0.0.1 * @type {?RegExp} */ - this.prefix = null; + this.prefix = this.prefix || null; /** * The length of the prefix used * @since 0.0.1 * @type {?number} */ - this.prefixLength = null; + this.prefixLength = typeof this.prefixLength === 'number' ? this.prefixLength : null; /** * A command prompt/argument handler @@ -47,7 +54,7 @@ module.exports = Structures.extend('Message', Message => { * @type {CommandPrompt} * @private */ - this.prompter = null; + this.prompter = this.prompter || null; /** * The responses to this message @@ -116,33 +123,7 @@ module.exports = Structures.extend('Message', Message => { */ get reactable() { if (!this.guild) return true; - return this.channel.readable && this.channel.permissionsFor(this.guild.me).has(FLAGS.ADD_REACTIONS, false); - } - - /** - * Gets the level id of this message (with respect to @{link Command#cooldownLevel}) - * @since 0.5.0 - * @type {?string} - * @readonly - * @private - */ - get levelID() { - if (!this.command) return null; - return this.guild ? this[this.command.cooldownLevel].id : this.author.id; - } - - /** - * Awaits a response from the author. - * @param {string} text The text to prompt the author - * @param {number} [time=30000] The time to wait before giving up - * @returns {KlasaMessage} - */ - async prompt(text, time = 30000) { - const message = await this.channel.send(text); - const responses = await this.channel.awaitMessages(msg => msg.author === this.author, { time, max: 1 }); - message.delete(); - if (responses.size === 0) throw this.language.get('MESSAGE_PROMPT_TIMEOUT'); - return responses.first(); + return this.channel.readable && this.channel.permissionsFor(this.guild.me).has([FLAGS.ADD_REACTIONS, FLAGS.READ_MESSAGE_HISTORY], false); } /** @@ -268,6 +249,7 @@ module.exports = Structures.extend('Message', Message => { patch(data) { super.patch(data); this.language = this.guild ? this.guild.language : this.client.languages.default; + this._parseCommand(); } /** @@ -285,32 +267,124 @@ module.exports = Structures.extend('Message', Message => { * @type {Language} */ this.language = this.guild ? this.guild.language : this.client.languages.default; + + /** + * The guild level settings for this context (guild || default) + * @since 0.5.0 + * @type {Settings} + */ + this.guildSettings = this.guild ? this.guild.settings : this.client.gateways.guilds.defaults; + + this._parseCommand(); + } + + /** + * Parses this message as a command + * @since 0.5.0 + * @private + */ + _parseCommand() { + // Clear existing command state so edits to non-commands do not re-run commands + this.prefix = null; + this.prefixLength = null; + this.commandText = null; + this.command = null; + this.prompter = null; + + try { + const prefix = this._mentionPrefix() || this._customPrefix() || this._naturalPrefix() || this._prefixLess(); + + if (!prefix) return; + + this.prefix = prefix.regex; + this.prefixLength = prefix.length; + this.commandText = this.content.slice(prefix.length).trim().split(' ')[0].toLowerCase(); + this.command = this.client.commands.get(this.commandText) || null; + + if (!this.command) return; + + this.prompter = this.command.usage.createPrompt(this, { + flagSupport: this.command.flagSupport, + quotedStringSupport: this.command.quotedStringSupport, + time: this.command.promptTime, + limit: this.command.promptLimit + }); + } catch (error) { + return; + } + } + + /** + * Checks if the per-guild or default prefix is used + * @since 0.5.0 + * @returns {CachedPrefix | null} + * @private + */ + _customPrefix() { + if (!this.guildSettings.prefix) return null; + for (const prf of Array.isArray(this.guildSettings.prefix) ? this.guildSettings.prefix : [this.guildSettings.prefix]) { + const testingPrefix = this.constructor.prefixes.get(prf) || this.constructor.generateNewPrefix(prf, this.client.options.prefixCaseInsensitive ? 'i' : ''); + if (testingPrefix.regex.test(this.content)) return testingPrefix; + } + return null; } /** - * Register's this message as a Command Message + * Checks if the mention was used as a prefix * @since 0.5.0 - * @param {Object} commandInfo The info about the command and prefix used - * @property {Command} command The command run - * @property {RegExp} prefix The prefix used - * @property {number} prefixLength The length of the prefix used - * @returns {this} + * @returns {CachedPrefix | null} * @private */ - _registerCommand({ command, prefix, prefixLength }) { - this.command = command; - this.prefix = prefix; - this.prefixLength = prefixLength; - this.prompter = this.command.usage.createPrompt(this, { - quotedStringSupport: this.command.quotedStringSupport, - time: this.command.promptTime, - limit: this.command.promptLimit - }); - this.client.emit('commandRun', this, this.command, this.args); - return this; + _mentionPrefix() { + const mentionPrefix = this.client.mentionPrefix.exec(this.content); + return mentionPrefix ? { length: mentionPrefix[0].length, regex: this.client.mentionPrefix } : null; + } + + /** + * Checks if the natural prefix is used + * @since 0.5.0 + * @returns {CachedPrefix | null} + * @private + */ + _naturalPrefix() { + if (this.guildSettings.disableNaturalPrefix || !this.client.options.regexPrefix) return null; + const results = this.client.options.regexPrefix.exec(this.content); + return results ? { length: results[0].length, regex: this.client.options.regexPrefix } : null; + } + + /** + * Checks if a prefixless scenario is possible + * @since 0.5.0 + * @returns {CachedPrefix | null} + * @private + */ + _prefixLess() { + return this.client.options.noPrefixDM && this.channel.type === 'dm' ? { length: 0, regex: null } : null; + } + + /** + * Caches a new prefix regexp + * @since 0.5.0 + * @param {string} prefix The prefix to store + * @param {string} flags The flags for the RegExp + * @returns {CachedPrefix} + * @private + */ + static generateNewPrefix(prefix, flags) { + const prefixObject = { length: prefix.length, regex: new RegExp(`^${regExpEsc(prefix)}`, flags) }; + this.prefixes.set(prefix, prefixObject); + return prefixObject; } } + /** + * Cache of RegExp prefixes + * @since 0.5.0 + * @type {Map} + * @private + */ + KlasaMessage.prefixes = new Map(); + return KlasaMessage; }); diff --git a/src/lib/schedule/Schedule.js b/src/lib/schedule/Schedule.js index 17762ef379..a4629d7ab9 100644 --- a/src/lib/schedule/Schedule.js +++ b/src/lib/schedule/Schedule.js @@ -216,7 +216,7 @@ class Schedule { * @private */ _clearInterval() { - clearInterval(this._interval); + this.client.clearInterval(this._interval); this._interval = null; } diff --git a/src/lib/schedule/ScheduledTask.js b/src/lib/schedule/ScheduledTask.js index e400a447a6..bd7d311f68 100644 --- a/src/lib/schedule/ScheduledTask.js +++ b/src/lib/schedule/ScheduledTask.js @@ -85,7 +85,7 @@ class ScheduledTask { /** * If the task should catch up in the event the bot is down * @since 0.5.0 - * @type {string} + * @type {boolean} */ this.catchUp = 'catchUp' in options ? options.catchUp : true; @@ -236,8 +236,7 @@ class ScheduledTask { * @private */ static _generateID(client) { - const id = client.shard ? (Array.isArray(client.shard.id) ? client.shard.id[0] : client.shard.id).toString(36) : ''; - return Date.now().toString(36) + id; + return `${Date.now().toString(36)}${client.options.shards[0].toString(36)}`; } /** diff --git a/src/lib/structures/Command.js b/src/lib/structures/Command.js index 817b32e7bf..ce97b3a8df 100644 --- a/src/lib/structures/Command.js +++ b/src/lib/structures/Command.js @@ -2,7 +2,6 @@ const { Permissions } = require('discord.js'); const AliasPiece = require('./base/AliasPiece'); const Usage = require('../usage/Usage'); const CommandUsage = require('../usage/CommandUsage'); -const RateLimitManager = require('../util/RateLimitManager'); const { isFunction } = require('../util/util'); /** @@ -28,6 +27,7 @@ class Command extends AliasPiece { * @property {boolean} [deletable=false] If the responses should be deleted if the triggering message is deleted * @property {(string|Function)} [description=''] The help description for the command * @property {ExtendedHelp} [extendedHelp] Extended help strings + * @property {boolean} [flagSupport=true] Whether flags should be parsed or not * @property {boolean} [guarded=false] If the command can be disabled on a guild level (does not effect global disable) * @property {boolean} [hidden=false] If the command should be hidden * @property {boolean} [nsfw=false] If the command should only run in nsfw channels @@ -44,14 +44,13 @@ class Command extends AliasPiece { /** * @since 0.0.1 - * @param {KlasaClient} client The Klasa Client * @param {CommandStore} store The Command store * @param {Array} file The path from the pieces folder to the command file * @param {string} directory The base directory to the pieces folder * @param {CommandOptions} [options={}] Optional Command settings */ - constructor(client, store, file, directory, options = {}) { - super(client, store, file, directory, options); + constructor(store, file, directory, options = {}) { + super(store, file, directory, options); this.name = this.name.toLowerCase(); @@ -146,6 +145,13 @@ class Command extends AliasPiece { */ this.promptTime = options.promptTime; + /** + * Whether to use flag support for this command or not + * @since 0.2.1 + * @type {boolean} + */ + this.flagSupport = options.flagSupport; + /** * Whether to use quoted string support for this command or not * @since 0.2.1 @@ -179,7 +185,7 @@ class Command extends AliasPiece { * @since 0.0.1 * @type {CommandUsage} */ - this.usage = new CommandUsage(client, options.usage, options.usageDelim, this); + this.usage = new CommandUsage(this.client, options.usage, options.usageDelim, this); /** * The level at which cooldowns should apply @@ -191,31 +197,18 @@ class Command extends AliasPiece { if (!['author', 'channel', 'guild'].includes(this.cooldownLevel)) throw new Error('Invalid cooldownLevel'); /** - * Any active cooldowns for the command - * @since 0.0.1 - * @type {RateLimitManager} - * @private + * The number of times this command can be run before ratelimited by the cooldown + * @since 0.5.0 + * @type {number} */ - this.cooldowns = new RateLimitManager(options.bucket, options.cooldown * 1000); - } + this.bucket = options.bucket; - /** - * The number of times this command can be run before ratelimited by the cooldown - * @since 0.5.0 - * @type {number} - * @readonly - */ - get bucket() { - return this.cooldowns.bucket; - } - /** - * The cooldown in seconds this command has - * @since 0.0.1 - * @type {number} - * @readonly - */ - get cooldown() { - return this.cooldowns.cooldown / 1000; + /** + * The amount of time before the users can run the command again in seconds based on cooldownLevel + * @since 0.5.0 + * @type {number} + */ + this.cooldown = options.cooldown; } /** diff --git a/src/lib/structures/Event.js b/src/lib/structures/Event.js index eb08460ab6..167367c314 100644 --- a/src/lib/structures/Event.js +++ b/src/lib/structures/Event.js @@ -17,14 +17,13 @@ class Event extends Piece { /** * @since 0.0.1 - * @param {KlasaClient} client The Klasa client * @param {EventStore} store The Event Store * @param {string} file The path from the pieces folder to the event file * @param {string} directory The base directory to the pieces folder * @param {EventOptions} [options={}] Optional Event settings */ - constructor(client, store, file, directory, options = {}) { - super(client, store, file, directory, options); + constructor(store, file, directory, options = {}) { + super(store, file, directory, options); /** * If this event should only be run once and then unloaded diff --git a/src/lib/structures/Extendable.js b/src/lib/structures/Extendable.js index bcee5e9b9a..f4bb4dbf71 100644 --- a/src/lib/structures/Extendable.js +++ b/src/lib/structures/Extendable.js @@ -22,14 +22,13 @@ class Extendable extends Piece { /** * @since 0.0.1 - * @param {KlasaClient} client The klasa client * @param {ExtendableStore} store The extendable store * @param {string[]} file The path from the pieces folder to the extendable file * @param {string} directory The base directory to the pieces folder * @param {ExtendableOptions} [options={}] The options for this extendable */ - constructor(client, store, file, directory, options = {}) { - super(client, store, file, directory, options); + constructor(store, file, directory, options = {}) { + super(store, file, directory, options); const staticPropertyNames = Object.getOwnPropertyNames(this.constructor) .filter(name => !['length', 'prototype', 'name'].includes(name)); diff --git a/src/lib/structures/Inhibitor.js b/src/lib/structures/Inhibitor.js index 5e2a699af5..afe3b25781 100644 --- a/src/lib/structures/Inhibitor.js +++ b/src/lib/structures/Inhibitor.js @@ -15,14 +15,13 @@ class Inhibitor extends Piece { /** * @since 0.0.1 - * @param {KlasaClient} client The Klasa client * @param {InhibitorStore} store The Inhibitor Store * @param {string} file The path from the pieces folder to the inhibitor file * @param {string} directory The base directory to the pieces folder * @param {InhibitorOptions} [options={}] Optional Inhibitor settings */ - constructor(client, store, file, directory, options = {}) { - super(client, store, file, directory, options); + constructor(store, file, directory, options = {}) { + super(store, file, directory, options); /** * If this inhibitor is meant for spamProtection (disables the inhibitor while generating help) diff --git a/src/lib/structures/Language.js b/src/lib/structures/Language.js index e479388cc5..80e246f178 100644 --- a/src/lib/structures/Language.js +++ b/src/lib/structures/Language.js @@ -45,7 +45,7 @@ class Language extends Piece { try { const CorePiece = (req => req.default || req)(require(loc)); if (!isClass(CorePiece)) return; - const coreLang = new CorePiece(this.client, this.store, this.file, true); + const coreLang = new CorePiece(this.store, this.file, core); this.language = mergeDefault(coreLang.language, this.language); } catch (error) { return; diff --git a/src/lib/structures/Monitor.js b/src/lib/structures/Monitor.js index 134276774d..88e4a5b5d2 100644 --- a/src/lib/structures/Monitor.js +++ b/src/lib/structures/Monitor.js @@ -22,14 +22,13 @@ class Monitor extends Piece { /** * @since 0.0.1 - * @param {KlasaClient} client The Klasa client * @param {MonitorStore} store The Monitor Store * @param {string} file The path from the pieces folder to the monitor file * @param {string} directory The base directory to the pieces folder * @param {MonitorOptions} [options={}] Optional Monitor settings */ - constructor(client, store, file, directory, options = {}) { - super(client, store, file, directory, options); + constructor(store, file, directory, options = {}) { + super(store, file, directory, options); /** * The types of messages allowed for this monitor diff --git a/src/lib/structures/base/AliasPiece.js b/src/lib/structures/base/AliasPiece.js index 5648c110f2..95c361888e 100644 --- a/src/lib/structures/base/AliasPiece.js +++ b/src/lib/structures/base/AliasPiece.js @@ -15,14 +15,13 @@ class AliasPiece extends Piece { /** * @since 0.0.1 - * @param {KlasaClient} client The klasa client * @param {Store} store The store this piece is for * @param {string[]} file The path from the pieces folder to the extendable file * @param {string} directory The base directory to the pieces folder * @param {AliasPieceOptions} [options={}] The options for this piece */ - constructor(client, store, file, directory, options = {}) { - super(client, store, file, directory, options); + constructor(store, file, directory, options = {}) { + super(store, file, directory, options); /** * The aliases for this piece diff --git a/src/lib/structures/base/Piece.js b/src/lib/structures/base/Piece.js index 1212db40c3..cbc2593bdc 100644 --- a/src/lib/structures/base/Piece.js +++ b/src/lib/structures/base/Piece.js @@ -26,14 +26,13 @@ class Piece { /** * @since 0.0.1 - * @param {KlasaClient} client The klasa client * @param {Store} store The store this piece is for - * @param {string[]} file The path from the pieces folder to the extendable file + * @param {string[]} file The path from the pieces folder to the piece file * @param {string} directory The base directory to the pieces folder * @param {PieceOptions} [options={}] The options for this piece */ - constructor(client, store, file, directory, options = {}) { - const defaults = client.options.pieceDefaults[store.name]; + constructor(store, file, directory, options = {}) { + const defaults = store.client.options.pieceDefaults[store.name]; if (defaults) options = mergeDefault(defaults, options); /** @@ -41,7 +40,7 @@ class Piece { * @since 0.0.1 * @type {KlasaClient} */ - this.client = client; + this.client = store.client; /** * The file location where this Piece is stored diff --git a/src/lib/structures/base/Store.js b/src/lib/structures/base/Store.js index 7cee2f964a..862e8d61c0 100644 --- a/src/lib/structures/base/Store.js +++ b/src/lib/structures/base/Store.js @@ -79,7 +79,7 @@ class Store extends Collection { * @protected */ registerCoreDirectory(directory) { - this.coreDirectories.add(directory + this.name); + this.coreDirectories.add(join(directory, this.name)); return this; } @@ -105,7 +105,7 @@ class Store extends Collection { try { const Piece = (req => req.default || req)(require(loc)); if (!isClass(Piece)) throw new TypeError('The exported structure is not a class.'); - piece = this.set(new Piece(this.client, this, file, directory)); + piece = this.set(new Piece(this, file, directory)); } catch (error) { if (this.client.listenerCount('wtf')) this.client.emit('wtf', `Failed to load file '${loc}'. Error:\n${error.stack || error}`); else this.client.console.wtf(`Failed to load file '${loc}'. Error:\n${error.stack || error}`); diff --git a/src/lib/usage/CommandUsage.js b/src/lib/usage/CommandUsage.js index 32809a5901..269f6aabb6 100644 --- a/src/lib/usage/CommandUsage.js +++ b/src/lib/usage/CommandUsage.js @@ -58,7 +58,7 @@ class CommandUsage extends Usage { */ fullUsage(message) { let prefix = message.prefixLength ? message.content.slice(0, message.prefixLength) : message.guildSettings.prefix; - if (message.prefix === this.client.monitors.get('commandHandler').prefixMention) prefix = `@${this.client.user.tag}`; + if (message.prefix === this.client.mentionPrefix) prefix = `@${this.client.user.tag}`; else if (Array.isArray(prefix)) [prefix] = prefix; return `${prefix.length !== 1 ? `${prefix} ` : prefix}${this.nearlyFullUsage}`; } diff --git a/src/lib/usage/Possible.js b/src/lib/usage/Possible.js index b52553e85e..d0c56903cb 100644 --- a/src/lib/usage/Possible.js +++ b/src/lib/usage/Possible.js @@ -22,7 +22,7 @@ class Possible { * @since 0.2.1 * @type {string} */ - this.type = type.toLowerCase(); + this.type = type; /** * The min of this possible diff --git a/src/lib/usage/Tag.js b/src/lib/usage/Tag.js index 80fd745c12..7ead70d456 100644 --- a/src/lib/usage/Tag.js +++ b/src/lib/usage/Tag.js @@ -71,7 +71,7 @@ class Tag { const types = []; members = this.parseTrueMembers(members); return members.map((member, i) => { - const current = `${members}: at tag #${count} at bound #${i + 1}`; + const current = `${members.join('|')}: at tag #${count} at bound #${i + 1}`; let possible; try { possible = new Possible(this.pattern.exec(member)); diff --git a/src/lib/usage/TextPrompt.js b/src/lib/usage/TextPrompt.js index 36f28cae2b..2178bb7377 100644 --- a/src/lib/usage/TextPrompt.js +++ b/src/lib/usage/TextPrompt.js @@ -9,9 +9,12 @@ class TextPrompt { /** * @typedef {Object} TextPromptOptions + * @property {KlasaUser} [target=message.author] The intended target of this TextPrompt, if someone other than the author + * @property {external:TextBasedChannel} [channel=message.channel] The channel to prompt in, if other than this channel * @property {number} [limit=Infinity] The number of re-prompts before this TextPrompt gives up * @property {number} [time=30000] The time-limit for re-prompting * @property {boolean} [quotedStringSupport=false] Whether this prompt should respect quoted strings + * @property {boolean} [flagSupport=true] Whether this prompt should respect flags */ /** @@ -39,6 +42,20 @@ class TextPrompt { */ this.message = message; + /** + * The target this prompt is for + * @since 0.5.0 + * @type {KlasaUser} + */ + this.target = options.target || message.author; + + /** + * The channel to prompt in + * @since 0.5.0 + * @type {external:TextBasedChannel} + */ + this.channel = options.channel || message.channel; + /** * The usage for this prompt * @since 0.5.0 @@ -95,6 +112,13 @@ class TextPrompt { */ this.quotedStringSupport = options.quotedStringSupport; + /** + * Whether this prompt should respect flags + * @since 0.5.0 + * @type {boolean} + */ + this.flagSupport = options.flagSupport; + /** * Whether the current usage is a repeating arg * @since 0.0.1 @@ -138,16 +162,30 @@ class TextPrompt { /** * Runs the custom prompt. * @since 0.5.0 - * @param {string} prompt The message to initially prompt with + * @param {StringResolvable | MessageOptions | MessageAdditions | APIMessage} prompt The message to initially prompt with * @returns {any[]} The parameters resolved */ async run(prompt) { - const message = await this.message.prompt(prompt, this.time); + const message = await this.prompt(prompt); this.responses.set(message.id, message); this._setup(message.content); return this.validateArgs(); } + /** + * Prompts the target for a response + * @param {StringResolvable | MessageOptions | MessageAdditions | APIMessage} text The text to prompt + * @returns {KlasaMessage} + * @private + */ + async prompt(text) { + const message = await this.channel.send(text); + const responses = await message.channel.awaitMessages(msg => msg.author === this.target, { time: this.time, max: 1 }); + message.delete(); + if (responses.size === 0) throw this.message.language.get('MESSAGE_PROMPT_TIMEOUT'); + return responses.first(); + } + /** * Collects missing required arguments. * @since 0.5.0 @@ -159,15 +197,14 @@ class TextPrompt { this._prompted++; if (this.typing) this.message.channel.stopTyping(); const possibleAbortOptions = this.message.language.get('TEXT_PROMPT_ABORT_OPTIONS'); - const message = await this.message.prompt( - this.message.language.get('MONITOR_COMMAND_HANDLER_REPROMPT', `<@!${this.message.author.id}>`, prompt, this.time / 1000, possibleAbortOptions), - this.time + const edits = this.message.edits.length; + const message = await this.prompt( + this.message.language.get('MONITOR_COMMAND_HANDLER_REPROMPT', `<@!${this.target.id}>`, prompt, this.time / 1000, possibleAbortOptions) ); + if (this.message.edits.length !== edits || message.prefix || possibleAbortOptions.includes(message.content.toLowerCase())) throw this.message.language.get('MONITOR_COMMAND_HANDLER_ABORTED'); this.responses.set(message.id, message); - if (possibleAbortOptions.includes(message.content.toLowerCase())) throw this.message.language.get('MONITOR_COMMAND_HANDLER_ABORTED'); - if (this.typing) this.message.channel.startTyping(); this.args[this.args.lastIndexOf(null)] = message.content; this.reprompted = true; @@ -187,9 +224,8 @@ class TextPrompt { let message; const possibleCancelOptions = this.message.language.get('TEXT_PROMPT_ABORT_OPTIONS'); try { - message = await this.message.prompt( - this.message.language.get('MONITOR_COMMAND_HANDLER_REPEATING_REPROMPT', `<@!${this.message.author.id}>`, this._currentUsage.possibles[0].name, this.time / 1000, possibleCancelOptions), - this.time + message = await this.prompt( + this.message.language.get('MONITOR_COMMAND_HANDLER_REPEATING_REPROMPT', `<@!${this.message.author.id}>`, this._currentUsage.possibles[0].name, this.time / 1000, possibleCancelOptions) ); this.responses.set(message.id, message); } catch (err) { @@ -314,7 +350,7 @@ class TextPrompt { * @private */ _setup(original) { - const { content, flags } = this.constructor.getFlags(original, this.usage.usageDelim); + const { content, flags } = this.flagSupport ? this.constructor.getFlags(original, this.usage.usageDelim) : { content: original, flags: {} }; this.flags = flags; this.args = this.quotedStringSupport ? this.constructor.getQuotedStringArgs(content, this.usage.usageDelim).map(arg => arg.trim()) : diff --git a/src/lib/util/Timestamp.js b/src/lib/util/Timestamp.js index 5185b79088..4fa9ff95bd 100644 --- a/src/lib/util/Timestamp.js +++ b/src/lib/util/Timestamp.js @@ -1,5 +1,77 @@ const { TIME: { SECOND, MINUTE, DAY, DAYS, MONTHS, TIMESTAMP: { TOKENS } } } = require('./constants'); +/* eslint-disable max-len */ +const tokens = new Map([ + // Dates + ['Y', time => String(time.getFullYear()).slice(2)], + ['YY', time => String(time.getFullYear()).slice(2)], + ['YYY', time => String(time.getFullYear())], + ['YYYY', time => String(time.getFullYear())], + ['Q', time => String((time.getMonth() + 1) / 3)], + ['M', time => String(time.getMonth() + 1)], + ['MM', time => String(time.getMonth() + 1).padStart(2, '0')], + ['MMM', time => MONTHS[time.getMonth()]], + ['MMMM', time => MONTHS[time.getMonth()]], + ['D', time => String(time.getDate())], + ['DD', time => String(time.getDate()).padStart(2, '0')], + ['DDD', time => { + const start = new Date(time.getFullYear(), 0, 0); + const diff = ((time.getMilliseconds() - start.getMilliseconds()) + (start.getTimezoneOffset() - time.getTimezoneOffset())) * MINUTE; + return String(Math.floor(diff / DAY)); + }], + ['DDDD', time => { + const start = new Date(time.getFullYear(), 0, 0); + const diff = ((time.getMilliseconds() - start.getMilliseconds()) + (start.getTimezoneOffset() - time.getTimezoneOffset())) * MINUTE; + return String(Math.floor(diff / DAY)); + }], + ['d', time => { + const day = String(time.getDate()); + if (day !== '11' && day.endsWith('1')) return `${day}st`; + if (day !== '12' && day.endsWith('2')) return `${day}nd`; + if (day !== '13' && day.endsWith('3')) return `${day}rd`; + return `${day}th`; + }], + ['dd', time => DAYS[time.getDay()].slice(0, 2)], + ['ddd', time => DAYS[time.getDay()].slice(0, 3)], + ['dddd', time => DAYS[time.getDay()]], + ['X', time => String(time.valueOf() / SECOND)], + ['x', time => String(time.valueOf())], + + // Locales + ['H', time => String(time.getHours())], + ['HH', time => String(time.getHours()).padStart(2, '0')], + ['h', time => String(time.getHours() % 12 || 12)], + ['hh', time => String(time.getHours() % 12 || 12).padStart(2, '0')], + ['a', time => time.getHours() < 12 ? 'am' : 'pm'], + ['A', time => time.getHours() < 12 ? 'AM' : 'PM'], + ['m', time => String(time.getMinutes())], + ['mm', time => String(time.getMinutes()).padStart(2, '0')], + ['s', time => String(time.getSeconds())], + ['ss', time => String(time.getSeconds()).padStart(2, '0')], + ['S', time => String(time.getMilliseconds())], + ['SS', time => String(time.getMilliseconds()).padStart(2, '0')], + ['SSS', time => String(time.getMilliseconds()).padStart(3, '0')], + ['T', time => `${String(time.getHours() % 12 || 12)}:${String(time.getMinutes()).padStart(2, '0')} ${time.getHours() < 12 ? 'AM' : 'PM'}`], + ['t', time => `${String(time.getHours() % 12 || 12)}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')} ${time.getHours() < 12 ? 'am' : 'pm'}`], + ['L', time => `${String(time.getMonth() + 1).padStart(2, '0')}/${String(time.getDate()).padStart(2, '0')}/${String(time.getFullYear())}`], + ['l', time => `${String(time.getMonth() + 1)}/${String(time.getDate()).padStart(2, '0')}/${String(time.getFullYear())}`], + ['LL', time => `${MONTHS[time.getMonth()]} ${String(time.getDate()).padStart(2, '0')}, ${String(time.getFullYear())}`], + ['ll', time => `${MONTHS[time.getMonth()].slice(0, 3)} ${String(time.getDate()).padStart(2, '0')}, ${String(time.getFullYear())}`], + ['LLL', time => `${MONTHS[time.getMonth()]} ${String(time.getDate()).padStart(2, '0')}, ${String(time.getFullYear())} ${String(time.getHours() % 12 || 12)}:${String(time.getMinutes()).padStart(2, '0')} ${time.getHours() < 12 ? 'AM' : 'PM'}`], + ['lll', time => `${MONTHS[time.getMonth()].slice(0, 3)} ${String(time.getDate()).padStart(2, '0')}, ${String(time.getFullYear())} ${String(time.getHours() % 12 || 12)}:${String(time.getMinutes()).padStart(2, '0')} ${time.getHours() < 12 ? 'AM' : 'PM'}`], + ['LLLL', time => `${DAYS[time.getDay()]}, ${MONTHS[time.getMonth()]} ${String(time.getDate()).padStart(2, '0')}, ${String(time.getFullYear())} ${String(time.getHours() % 12 || 12)}:${String(time.getMinutes()).padStart(2, '0')} ${time.getHours() < 12 ? 'AM' : 'PM'}`], + ['llll', time => `${DAYS[time.getDay()].slice(0, 3)} ${MONTHS[time.getMonth()].slice(0, 3)} ${String(time.getDate()).padStart(2, '0')}, ${String(time.getFullYear())} ${String(time.getHours() % 12 || 12)}:${String(time.getMinutes()).padStart(2, '0')} ${time.getHours() < 12 ? 'AM' : 'PM'}`], + ['Z', time => { + const offset = time.getTimezoneOffset(); + return `${offset >= 0 ? '+' : '-'}${String(offset / -60).padStart(2, '0')}:${String(offset % 60).padStart(2, '0')}`; + }], + ['ZZ', time => { + const offset = time.getTimezoneOffset(); + return `${offset >= 0 ? '+' : '-'}${String(offset / -60).padStart(2, '0')}:${String(offset % 60).padStart(2, '0')}`; + }] +]); +/* eslint-enable max-len */ + /** * Klasa's Timestamp class, parses the pattern once, displays the desired Date or UNIX timestamp with the selected pattern. */ @@ -106,7 +178,7 @@ class Timestamp { static _display(template, time) { let output = ''; const parsedTime = Timestamp._resolveDate(time); - for (const { content, type } of template) output += content || Timestamp[type](parsedTime); + for (const { content, type } of template) output += content || tokens.get(type)(parsedTime); return output; } @@ -122,9 +194,10 @@ class Timestamp { for (let i = 0; i < pattern.length; i++) { let current = ''; const currentChar = pattern[i]; - if (currentChar in TOKENS) { + const tokenMax = TOKENS.get(currentChar); + if (typeof tokenMax === 'number') { current += currentChar; - while (pattern[i + 1] === currentChar && current.length < TOKENS[currentChar]) current += pattern[++i]; + while (pattern[i + 1] === currentChar && current.length < tokenMax) current += pattern[++i]; template.push({ type: current, content: null }); } else if (currentChar === '[') { while (i + 1 < pattern.length && pattern[i + 1] !== ']') current += pattern[++i]; @@ -132,7 +205,7 @@ class Timestamp { template.push({ type: 'literal', content: current }); } else { current += currentChar; - while (i + 1 < pattern.length && !(pattern[i + 1] in TOKENS) && pattern[i + 1] !== '[') current += pattern[++i]; + while (i + 1 < pattern.length && !TOKENS.has(pattern[i + 1]) && pattern[i + 1] !== '[') current += pattern[++i]; template.push({ type: 'literal', content: current }); } } @@ -153,114 +226,12 @@ class Timestamp { } -/* eslint-disable id-length */ - +/** + * The timezone offset in seconds. + * @since 0.5.0 + * @type {number} + * @static + */ Timestamp.timezoneOffset = new Date().getTimezoneOffset() * 60000; -// Dates - -Timestamp.Y = -Timestamp.YY = time => String(time.getFullYear()).slice(0, 2); - -Timestamp.YYY = -Timestamp.YYYY = time => String(time.getFullYear()); - -Timestamp.Q = time => String((time.getMonth() + 1) / 3); - -Timestamp.M = time => String(time.getMonth() + 1); - -Timestamp.MM = time => String(time.getMonth() + 1).padStart(2, '0'); - -Timestamp.MMM = -Timestamp.MMMM = time => MONTHS[time.getMonth()]; - -Timestamp.D = time => String(time.getDate()); - -Timestamp.DD = time => String(time.getDate()).padStart(2, '0'); - -Timestamp.DDD = -Timestamp.DDDD = time => { - const start = new Date(time.getFullYear(), 0, 0); - const diff = ((time.getMilliseconds() - start.getMilliseconds()) + (start.getTimezoneOffset() - time.getTimezoneOffset())) * MINUTE; - return String(Math.floor(diff / DAY)); -}; - -Timestamp.d = time => { - const day = String(time.getDate()); - if (day !== '11' && day.endsWith('1')) return `${day}st`; - if (day !== '12' && day.endsWith('2')) return `${day}nd`; - if (day !== '13' && day.endsWith('3')) return `${day}rd`; - return `${day}th`; -}; - -Timestamp.dd = time => DAYS[time.getDay()].slice(0, 2); - -Timestamp.ddd = time => DAYS[time.getDay()].slice(0, 3); - -Timestamp.dddd = time => DAYS[time.getDay()]; - -Timestamp.X = time => String(time.valueOf() / SECOND); - -Timestamp.x = time => String(time.valueOf()); - -// Times - -Timestamp.H = time => String(time.getHours()); - -Timestamp.HH = time => String(time.getHours()).padStart(2, '0'); - -Timestamp.h = time => String(time.getHours() % 12 || 12); - -Timestamp.hh = time => String(time.getHours() % 12 || 12).padStart(2, '0'); - -Timestamp.a = time => time.getHours() < 12 ? 'am' : 'pm'; - -Timestamp.A = time => time.getHours() < 12 ? 'AM' : 'PM'; - -Timestamp.m = time => String(time.getMinutes()); - -Timestamp.mm = time => String(time.getMinutes()).padStart(2, '0'); - -Timestamp.s = time => String(time.getSeconds()); - -Timestamp.ss = time => String(time.getSeconds()).padStart(2, '0'); - -Timestamp.S = time => String(time.getMilliseconds()); - -Timestamp.SS = time => String(time.getMilliseconds()).padStart(2, '0'); - -Timestamp.SSS = time => String(time.getMilliseconds()).padStart(3, '0'); - -// Locales - -/* eslint max-len:0 new-cap:0 */ - -Timestamp.T = (time) => `${Timestamp.h(time)}:${Timestamp.mm(time)} ${Timestamp.A(time)}`; - -Timestamp.t = (time) => `${Timestamp.h(time)}:${Timestamp.mm(time)}:${Timestamp.ss(time)} ${Timestamp.A(time)}`; - -Timestamp.L = (time) => `${Timestamp.MM(time)}/${Timestamp.DD(time)}/${Timestamp.YYYY(time)}`; - -Timestamp.l = (time) => `${Timestamp.M(time)}/${Timestamp.DD(time)}/${Timestamp.YYYY(time)}`; - -Timestamp.LL = (time) => `${Timestamp.MMMM(time)} ${Timestamp.DD(time)}, ${Timestamp.YYYY(time)}`; - -Timestamp.ll = (time) => `${Timestamp.MMMM(time).slice(0, 3)} ${Timestamp.DD(time)}, ${Timestamp.YYYY(time)}`; - -Timestamp.LLL = (time) => `${Timestamp.LL(time)} ${Timestamp.T(time)}`; - -Timestamp.lll = (time) => `${Timestamp.ll(time)} ${Timestamp.T(time)}`; - -Timestamp.LLLL = (time) => `${Timestamp.dddd(time)}, ${Timestamp.LLL(time)}`; - -Timestamp.llll = (time) => `${Timestamp.ddd(time)} ${Timestamp.lll(time)}`; - -Timestamp.Z = -Timestamp.ZZ = time => { - const offset = time.getTimezoneOffset(); - return `${offset >= 0 ? '+' : '-'}${String(offset / -60).padStart(2, '0')}:${String(offset % 60).padStart(2, '0')}`; -}; - -/* eslint-enable id-length */ - module.exports = Timestamp; diff --git a/src/lib/util/Type.js b/src/lib/util/Type.js index 69313afa77..6a3d97ef3b 100644 --- a/src/lib/util/Type.js +++ b/src/lib/util/Type.js @@ -143,7 +143,7 @@ class Type { static resolve(value) { const type = typeof value; switch (type) { - case 'object': return value === null ? 'null' : value.constructor ? value.constructor.name : 'any'; + case 'object': return value === null ? 'null' : (value.constructor && value.constructor.name) || 'any'; case 'function': return `${value.constructor.name}(${value.length}-arity)`; case 'undefined': return 'void'; default: return type; diff --git a/src/lib/util/constants.js b/src/lib/util/constants.js index 86bf4a1d84..b6739791c6 100644 --- a/src/lib/util/constants.js +++ b/src/lib/util/constants.js @@ -32,13 +32,15 @@ exports.DEFAULTS = { customPromptDefaults: { time: 30000, limit: Infinity, - quotedStringSupport: false + quotedStringSupport: false, + flagSupport: true }, gateways: { guilds: {}, users: {}, clientStorage: {} }, + owners: [], // eslint-disable-next-line no-process-env production: process.env.NODE_ENV === 'production', prefixCaseInsensitive: false, @@ -57,6 +59,7 @@ exports.DEFAULTS = { description: '', extendedHelp: language => language.get('COMMAND_HELP_NO_EXTENDED'), enabled: true, + flagSupport: true, guarded: false, hidden: false, nsfw: false, @@ -171,29 +174,27 @@ exports.TIME = { MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], TIMESTAMP: { - TOKENS: { - /* eslint-disable id-length */ - Y: 4, - Q: 1, - M: 4, - D: 4, - d: 4, - X: 1, - x: 1, - H: 2, - h: 2, - a: 1, - A: 1, - m: 2, - s: 2, - S: 3, - Z: 2, - l: 4, - L: 4, - T: 1, - t: 1 - /* eslint-enable id-length */ - } + TOKENS: new Map([ + ['Y', 4], + ['Q', 1], + ['M', 4], + ['D', 4], + ['d', 4], + ['X', 1], + ['x', 1], + ['H', 2], + ['h', 2], + ['a', 1], + ['A', 1], + ['m', 2], + ['s', 2], + ['S', 3], + ['Z', 2], + ['l', 4], + ['L', 4], + ['T', 1], + ['t', 1] + ]) }, CRON: { diff --git a/src/lib/util/util.js b/src/lib/util/util.js index f7f78d9f8a..3323c0e41d 100644 --- a/src/lib/util/util.js +++ b/src/lib/util/util.js @@ -313,7 +313,7 @@ class Util { if ((guild instanceof GuildChannel) || (guild instanceof Message)) return guild.guild; } else if (type === 'string' && /^\d{17,19}$/.test(guild)) { - return client.guilds.get(guild) || null; + return client.guilds.cache.get(guild) || null; } return null; } diff --git a/src/monitors/commandHandler.js b/src/monitors/commandHandler.js index 75a732d4bd..dc3ef33933 100644 --- a/src/monitors/commandHandler.js +++ b/src/monitors/commandHandler.js @@ -1,67 +1,23 @@ -const { Monitor, Stopwatch, util: { regExpEsc } } = require('klasa'); +const { Monitor, Stopwatch } = require('klasa'); module.exports = class extends Monitor { constructor(...args) { super(...args, { ignoreOthers: false }); this.ignoreEdits = !this.client.options.commandEditing; - this.prefixes = new Map(); - this.prefixMention = null; - this.mentionOnly = null; - this.prefixFlags = this.client.options.prefixCaseInsensitive ? 'i' : ''; } async run(message) { if (message.guild && !message.guild.me) await message.guild.members.fetch(this.client.user); if (!message.channel.postable) return undefined; - if (this.mentionOnly.test(message.content)) return message.sendLocale('PREFIX_REMINDER', [message.guildSettings.prefix.length ? message.guildSettings.prefix : undefined]); - - const { commandText, prefix, prefixLength } = this.parseCommand(message); - if (!commandText) return undefined; - - const command = this.client.commands.get(commandText); - if (!command) return this.client.emit('commandUnknown', message, commandText, prefix, prefixLength); - - return this.runCommand(message._registerCommand({ command, prefix, prefixLength })); - } - - parseCommand(message) { - const result = this.customPrefix(message) || this.mentionPrefix(message) || this.naturalPrefix(message) || this.prefixLess(message); - return result ? { - commandText: message.content.slice(result.length).trim().split(' ')[0].toLowerCase(), - prefix: result.regex, - prefixLength: result.length - } : { commandText: false }; - } - - customPrefix({ content, guildSettings: { prefix } }) { - if (!prefix) return null; - for (const prf of Array.isArray(prefix) ? prefix : [prefix]) { - const testingPrefix = this.prefixes.get(prf) || this.generateNewPrefix(prf); - if (testingPrefix.regex.test(content)) return testingPrefix; + if (!message.commandText && message.prefix === this.client.mentionPrefix) { + return message.sendLocale('PREFIX_REMINDER', [message.guildSettings.prefix.length ? message.guildSettings.prefix : undefined]); } - return null; - } + if (!message.commandText) return undefined; + if (!message.command) return this.client.emit('commandUnknown', message, message.commandText, message.prefix, message.prefixLength); + this.client.emit('commandRun', message, message.command, message.args); - mentionPrefix({ content }) { - const prefixMention = this.prefixMention.exec(content); - return prefixMention ? { length: prefixMention[0].length, regex: this.prefixMention } : null; - } - - naturalPrefix({ content, guildSettings: { disableNaturalPrefix } }) { - if (disableNaturalPrefix || !this.client.options.regexPrefix) return null; - const results = this.client.options.regexPrefix.exec(content); - return results ? { length: results[0].length, regex: this.client.options.regexPrefix } : null; - } - - prefixLess({ channel: { type } }) { - return this.client.options.noPrefixDM && type === 'dm' ? { length: 0, regex: null } : null; - } - - generateNewPrefix(prefix) { - const prefixObject = { length: prefix.length, regex: new RegExp(`^${regExpEsc(prefix)}`, this.prefixFlags) }; - this.prefixes.set(prefix, prefixObject); - return prefixObject; + return this.runCommand(message); } async runCommand(message) { @@ -71,14 +27,18 @@ module.exports = class extends Monitor { await this.client.inhibitors.run(message, message.command); try { await message.prompter.run(); - const subcommand = message.command.subcommands ? message.params.shift() : undefined; - const commandRun = subcommand ? message.command[subcommand](message, message.params) : message.command.run(message, message.params); - timer.stop(); - const response = await commandRun; - this.client.finalizers.run(message, message.command, response, timer); - this.client.emit('commandSuccess', message, message.command, message.params, response); - } catch (error) { - this.client.emit('commandError', message, message.command, message.params, error); + try { + const subcommand = message.command.subcommands ? message.params.shift() : undefined; + const commandRun = subcommand ? message.command[subcommand](message, message.params) : message.command.run(message, message.params); + timer.stop(); + const response = await commandRun; + this.client.finalizers.run(message, message.command, response, timer); + this.client.emit('commandSuccess', message, message.command, message.params, response); + } catch (error) { + this.client.emit('commandError', message, message.command, message.params, error); + } + } catch (argumentError) { + this.client.emit('argumentError', message, message.command, message.params, argumentError); } } catch (response) { this.client.emit('commandInhibited', message, message.command, response); @@ -86,9 +46,4 @@ module.exports = class extends Monitor { if (this.client.options.typing) message.channel.stopTyping(); } - init() { - this.prefixMention = new RegExp(`^<@!?${this.client.user.id}>`); - this.mentionOnly = new RegExp(`^<@!?${this.client.user.id}>$`); - } - }; diff --git a/typings/index.d.ts b/typings/index.d.ts index a092468203..e553d90569 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3,6 +3,7 @@ declare module 'klasa' { import { ExecOptions } from 'child_process'; import { + APIMessage, BufferResolvable, CategoryChannel, Channel, @@ -19,6 +20,7 @@ declare module 'klasa' { GuildEmoji, GuildMember, Message, + MessageAdditions, MessageAttachment, MessageCollector, MessageEmbed, @@ -50,7 +52,6 @@ declare module 'klasa' { public constructor(options?: KlasaClientOptions); public login(token?: string): Promise; private validatePermissionLevels(): PermissionLevels; - private _ready(): Promise; public sweepMessages(lifetime?: number, commandLifeTime?: number): number; public static basePermissions: Permissions; @@ -71,12 +72,23 @@ declare module 'klasa' { public readonly language: Language; } + export interface CachedPrefix { + regex: RegExp; + length: number; + } + export class KlasaMessage extends Message { - private levelID: Snowflake | null; private prompter: CommandPrompt | null; private _responses: KlasaMessage[]; private _patch(data: any): void; - private _registerCommand(commandInfo: { command: Command, prefix: RegExp, prefixLength: number }): this; + private _parseCommand(): void; + private _customPrefix(): CachedPrefix | null; + private _mentionPrefix(): CachedPrefix | null; + private _naturalPrefix(): CachedPrefix | null; + private _prefixLess(): CachedPrefix | null; + private static generateNewPrefix(prefix: string, flags: string): CachedPrefix; + + private static prefixes: Map; } export class KlasaUser extends User {} @@ -345,7 +357,7 @@ declare module 'klasa' { //#region Pieces export abstract class Piece { - public constructor(client: KlasaClient, store: Store, file: string[], directory: string, options?: PieceOptions); + public constructor(store: Store, file: string[], directory: string, options?: PieceOptions); public readonly client: KlasaClient; public readonly type: string; public readonly path: string; @@ -365,13 +377,13 @@ declare module 'klasa' { } export abstract class AliasPiece extends Piece { - public constructor(client: KlasaClient, store: Store, file: string[], directory: string, options?: AliasPieceOptions); + public constructor(store: Store, file: string[], directory: string, options?: AliasPieceOptions); public aliases: Array; public toJSON(): AliasPieceJSON; } export abstract class Argument extends AliasPiece { - public constructor(client: KlasaClient, store: ArgumentStore, file: string[], directory: string, options?: ArgumentOptions); + public constructor(store: ArgumentStore, file: string[], directory: string, options?: ArgumentOptions); public aliases: string[]; public abstract run(arg: string | undefined, possible: Possible, message: KlasaMessage): any; public static regex: MentionRegex; @@ -379,12 +391,12 @@ declare module 'klasa' { } export abstract class Command extends AliasPiece { - public constructor(client: KlasaClient, store: CommandStore, file: string[], directory: string, options?: CommandOptions); + public constructor(store: CommandStore, file: string[], directory: string, options?: CommandOptions); public readonly bucket: number; public readonly category: string; public readonly cooldown: number; public readonly subCategory: string; - public readonly usageDelim: string; + public readonly usageDelim: string | null; public readonly usageString: string; public aliases: string[]; public requiredPermissions: Permissions; @@ -392,6 +404,7 @@ declare module 'klasa' { public deletable: boolean; public description: string | ((language: Language) => string); public extendedHelp: string | ((language: Language) => string); + public flagSupport: boolean; public fullCategory: string[]; public guarded: boolean; public hidden: boolean; @@ -404,7 +417,6 @@ declare module 'klasa' { public runIn: string[]; public subcommands: boolean; public usage: CommandUsage; - private cooldowns: RateLimitManager; public createCustomResolver(type: string, resolver: ArgResolverCustomMethod): this; public customizeResponse(name: string, response: string | ((message: KlasaMessage, possible: Possible) => string)): this; @@ -414,7 +426,7 @@ declare module 'klasa' { } export abstract class Event extends Piece { - public constructor(client: KlasaClient, store: EventStore, file: string[], directory: string, options?: EventOptions); + public constructor(store: EventStore, file: string[], directory: string, options?: EventOptions); public emitter: NodeJS.EventEmitter; public event: string; public once: boolean; @@ -430,7 +442,7 @@ declare module 'klasa' { } export abstract class Extendable extends Piece { - public constructor(client: KlasaClient, store: ExtendableStore, file: string[], directory: string, options?: ExtendableOptions); + public constructor(store: ExtendableStore, file: string[], directory: string, options?: ExtendableOptions); public readonly appliesTo: Array>; private staticPropertyDescriptors: PropertyDescriptorMap; private instancePropertyDescriptors: PropertyDescriptorMap; @@ -439,14 +451,14 @@ declare module 'klasa' { } export abstract class Finalizer extends Piece { - public constructor(client: KlasaClient, store: FinalizerStore, file: string[], directory: string, options?: FinalizerOptions); + public constructor(store: FinalizerStore, file: string[], directory: string, options?: FinalizerOptions); public abstract run(message: KlasaMessage, command: Command, response: KlasaMessage | KlasaMessage[] | null, runTime: Stopwatch): void; public toJSON(): PieceFinalizerJSON; protected _run(message: KlasaMessage, command: Command, response: KlasaMessage | KlasaMessage[] | null, runTime: Stopwatch): Promise; } export abstract class Inhibitor extends Piece { - public constructor(client: KlasaClient, store: InhibitorStore, file: string[], directory: string, options?: InhibitorOptions); + public constructor(store: InhibitorStore, file: string[], directory: string, options?: InhibitorOptions); public spamProtection: boolean; public abstract run(message: KlasaMessage, command: Command): void | boolean | string | Promise; public toJSON(): PieceInhibitorJSON; @@ -454,7 +466,7 @@ declare module 'klasa' { } export abstract class Language extends Piece { - public constructor(client: KlasaClient, store: LanguageStore, file: string[], directory: string, options?: LanguageOptions); + public constructor(store: LanguageStore, file: string[], directory: string, options?: LanguageOptions); public language: Record string | string[])>; public get(term: string, ...args: any[]): T; @@ -462,7 +474,7 @@ declare module 'klasa' { } export abstract class Monitor extends Piece { - public constructor(client: KlasaClient, store: MonitorStore, file: string[], directory: string, options?: MonitorOptions); + public constructor(store: MonitorStore, file: string[], directory: string, options?: MonitorOptions); public allowedTypes: MessageType[]; public ignoreBots: boolean; public ignoreEdits: boolean; @@ -484,7 +496,7 @@ declare module 'klasa' { } export abstract class Provider extends Piece { - public constructor(client: KlasaClient, store: ProviderStore, file: string[], directory: string, options?: ProviderOptions); + public constructor(store: ProviderStore, file: string[], directory: string, options?: ProviderOptions); public abstract create(table: string, entry: string, data: any): Promise; public abstract createTable(table: string, rows?: any[]): Promise; public abstract delete(table: string, entry: string): Promise; @@ -516,13 +528,13 @@ declare module 'klasa' { } export abstract class Task extends Piece { - public constructor(client: KlasaClient, store: TaskStore, file: string[], directory: string, options?: TaskOptions); - public abstract run(data?: any): Promise; + public constructor(store: TaskStore, file: string[], directory: string, options?: TaskOptions); + public abstract run(data?: any): unknown; public toJSON(): PieceTaskJSON; } export abstract class Serializer extends AliasPiece { - public constructor(client: KlasaClient, store: SerializerStore, file: string[], directory: string, options?: SerializerOptions); + public constructor(store: SerializerStore, file: string[], directory: string, options?: SerializerOptions); public serialize(data: any): PrimitiveType; public stringify(data: any): string; public toJSON(): PieceSerializerJSON; @@ -609,7 +621,7 @@ declare module 'klasa' { } export class CommandUsage extends Usage { - public constructor(client: KlasaClient, usageString: string, usageDelim: string, command: Command); + public constructor(client: KlasaClient, usageString: string, usageDelim: string | null, command: Command); public names: string[]; public commands: string; public nearlyFullUsage: string; @@ -643,9 +655,11 @@ declare module 'klasa' { } export class TextPrompt { - public constructor(message: KlasaMessage, usage: Usage, options: TextPromptOptions); + public constructor(message: KlasaMessage, usage: Usage, options?: TextPromptOptions); public readonly client: KlasaClient; public message: KlasaMessage; + public target: KlasaUser; + public channel: TextChannel | DMChannel; public usage: Usage | CommandUsage; public reprompted: boolean; public flags: Record; @@ -660,7 +674,8 @@ declare module 'klasa' { private _prompted: number; private _currentUsage: Tag; - public run(prompt: string): Promise; + public run(prompt: StringResolvable | MessageOptions | MessageAdditions | APIMessage): Promise; + private prompt(text: string): Promise; private reprompt(prompt: string): Promise; private repeatingPrompt(): Promise; private validateArgs(): Promise; @@ -678,11 +693,11 @@ declare module 'klasa' { } export class Usage { - public constructor(client: KlasaClient, usageString: string, usageDelim: string); + public constructor(client: KlasaClient, usageString: string, usageDelim: string | null); public readonly client: KlasaClient; public deliminatedUsage: string; public usageString: string; - public usageDelim: string; + public usageDelim: string | null; public parsedUsage: Tag[]; public customResolvers: Record; @@ -894,44 +909,9 @@ declare module 'klasa' { public displayUTC(time?: Date | number | string): string; public edit(pattern: string): this; + public static timezoneOffset: number; public static utc(time?: Date | number | string): Date; public static displayArbitrary(pattern: string, time?: Date | number | string): string; - - public static A(time: Date): string; - public static a(time: Date): string; - public static d(time: Date): string; - public static D(time: Date): string; - public static dd(time: Date): string; - public static DD(time: Date): string; - public static ddd(time: Date): string; - public static DDD(time: Date): string; - public static dddd(time: Date): string; - public static DDDD(time: Date): string; - public static h(time: Date): string; - public static H(time: Date): string; - public static hh(time: Date): string; - public static HH(time: Date): string; - public static m(time: Date): string; - public static M(time: Date): string; - public static mm(time: Date): string; - public static MM(time: Date): string; - public static MMM(time: Date): string; - public static MMMM(time: Date): string; - public static Q(time: Date): string; - public static S(time: Date): string; - public static s(time: Date): string; - public static ss(time: Date): string; - public static SS(time: Date): string; - public static SSS(time: Date): string; - public static x(time: Date): string; - public static X(time: Date): string; - public static Y(time: Date): string; - public static YY(time: Date): string; - public static YYY(time: Date): string; - public static YYYY(time: Date): string; - public static Z(time: Date): string; - public static ZZ(time: Date): string; - private static _resolveDate(time: Date | number | string): Date; private static _display(template: string, time: Date | number | string): string; private static _patch(pattern: string): TimestampObject[]; @@ -1012,7 +992,7 @@ declare module 'klasa' { gateways?: GatewaysOptions; language?: string; noPrefixDM?: boolean; - ownerID?: string; + owners?: string[]; permissionLevels?: PermissionLevels; pieceDefaults?: PieceDefaults; prefix?: string | string[]; @@ -1089,23 +1069,7 @@ declare module 'klasa' { DAYS: string[]; MONTHS: string[]; TIMESTAMP: { - TOKENS: { - Y: number; - Q: number; - M: number; - D: number; - d: number; - X: number; - x: number; - H: number; - h: number; - a: number; - A: number; - m: number; - s: number; - S: number; - Z: number; - }; + TOKENS: Map; }; CRON: { partRegex: RegExp; @@ -1290,6 +1254,7 @@ declare module 'klasa' { deletable?: boolean; description?: string | string[] | ((language: Language) => string | string[]); extendedHelp?: string | string[] | ((language: Language) => string | string[]); + flagSupport?: boolean; guarded?: boolean; hidden?: boolean; nsfw?: boolean; @@ -1324,7 +1289,7 @@ declare module 'klasa' { } export interface EventOptions extends PieceOptions { - emitter?: NodeJS.EventEmitter; + emitter?: NodeJS.EventEmitter | FilterKeyInstances; event?: string; once?: boolean; } @@ -1354,10 +1319,12 @@ declare module 'klasa' { } export interface PieceCommandJSON extends AliasPieceJSON, Filter, 'requiredPermissions' | 'usage'> { + category: string; + subCategory: string; requiredPermissions: string[]; usage: { usageString: string; - usageDelim: string; + usageDelim: string | null; nearlyFullUsage: string; }; } @@ -1381,9 +1348,12 @@ declare module 'klasa' { // Usage export interface TextPromptOptions { + channel?: TextChannel | DMChannel; limit?: number; - time?: number; quotedStringSupport?: boolean; + target?: KlasaUser; + time?: number; + flagSupport?: boolean; } // Util @@ -1603,6 +1573,11 @@ declare module 'klasa' { [P in keyof T]: P extends K ? unknown : T[P]; }; + type ValueOf = T[keyof T]; + type FilterKeyInstances = ValueOf<{ + [K in keyof O]: O[K] extends T ? K : never + }>; + export interface TitleCaseVariants extends Record { textchannel: 'TextChannel'; voicechannel: 'VoiceChannel'; @@ -1642,6 +1617,7 @@ declare module 'discord.js' { Schedule, ScheduledTask, SerializerStore, + Stopwatch, Settings, Store, Task, @@ -1652,7 +1628,7 @@ declare module 'discord.js' { export interface Client { constructor: typeof KlasaClient; readonly invite: string; - readonly owner: User | null; + readonly owners: Set; options: Required; userBaseDirectory: string; console: KlasaConsole; @@ -1674,15 +1650,18 @@ declare module 'discord.js' { application: ClientApplication; schedule: Schedule; ready: boolean; + mentionPrefix: RegExp | null; registerStore>(store: Store): KlasaClient; unregisterStore>(store: Store): KlasaClient; sweepMessages(lifetime?: number, commandLifeTime?: number): number; - on(event: 'commandError', listener: (message: KlasaMessage, command: Command, params: any[], error: Error) => void): this; + on(event: 'argumentError', listener: (message: KlasaMessage, command: Command, params: any[], error: string) => void): this; + on(event: 'commandError', listener: (message: KlasaMessage, command: Command, params: any[], error: Error | string) => void): this; on(event: 'commandInhibited', listener: (message: KlasaMessage, command: Command, response: string | Error) => void): this; on(event: 'commandRun', listener: (message: KlasaMessage, command: Command, params: any[], response: any) => void): this; on(event: 'commandSuccess', listener: (message: KlasaMessage, command: Command, params: any[], response: any) => void): this; on(event: 'commandUnknown', listener: (message: KlasaMessage, command: string, prefix: RegExp, prefixLength: number) => void): this; - on(event: 'finalizerError', listener: (message: KlasaMessage, response: KlasaMessage, runTime: Timestamp, finalizer: Finalizer, error: Error | string) => void): this; + on(event: 'finalizerError', listener: (message: KlasaMessage, command: Command, response: KlasaMessage, runTime: Stopwatch, finalizer: Finalizer, error: Error | string) => void): this; + on(event: 'klasaReady', listener: () => void): this; on(event: 'log', listener: (data: any) => void): this; on(event: 'monitorError', listener: (message: KlasaMessage, monitor: Monitor, error: Error | string) => void): this; on(event: 'pieceDisabled', listener: (piece: Piece) => void): this; @@ -1696,12 +1675,14 @@ declare module 'discord.js' { on(event: 'taskError', listener: (scheduledTask: ScheduledTask, task: Task, error: Error) => void): this; on(event: 'verbose', listener: (data: any) => void): this; on(event: 'wtf', listener: (failure: Error) => void): this; - once(event: 'commandError', listener: (message: KlasaMessage, command: Command, params: any[], error: Error) => void): this; + once(event: 'argumentError', listener: (message: KlasaMessage, command: Command, params: any[], error: string) => void): this; + once(event: 'commandError', listener: (message: KlasaMessage, command: Command, params: any[], error: Error | string) => void): this; once(event: 'commandInhibited', listener: (message: KlasaMessage, command: Command, response: string | Error) => void): this; once(event: 'commandRun', listener: (message: KlasaMessage, command: Command, params: any[], response: any) => void): this; once(event: 'commandSuccess', listener: (message: KlasaMessage, command: Command, params: any[], response: any) => void): this; once(event: 'commandUnknown', listener: (message: KlasaMessage, command: string, prefix: RegExp, prefixLength: number) => void): this; - once(event: 'finalizerError', listener: (message: KlasaMessage, response: KlasaMessage, runTime: Timestamp, finalizer: Finalizer, error: Error | string) => void): this; + once(event: 'finalizerError', listener: (message: KlasaMessage, command: Command, response: KlasaMessage, runTime: Stopwatch, finalizer: Finalizer, error: Error | string) => void): this; + once(event: 'klasaReady', listener: () => void): this; once(event: 'log', listener: (data: any) => void): this; once(event: 'monitorError', listener: (message: KlasaMessage, monitor: Monitor, error: Error | string) => void): this; once(event: 'pieceDisabled', listener: (piece: Piece) => void): this; @@ -1715,12 +1696,14 @@ declare module 'discord.js' { once(event: 'taskError', listener: (scheduledTask: ScheduledTask, task: Task, error: Error) => void): this; once(event: 'verbose', listener: (data: any) => void): this; once(event: 'wtf', listener: (failure: Error) => void): this; - off(event: 'commandError', listener: (message: KlasaMessage, command: Command, params: any[], error: Error) => void): this; + off(event: 'argumentError', listener: (message: KlasaMessage, command: Command, params: any[], error: string) => void): this; + off(event: 'commandError', listener: (message: KlasaMessage, command: Command, params: any[], error: Error | string) => void): this; off(event: 'commandInhibited', listener: (message: KlasaMessage, command: Command, response: string | Error) => void): this; off(event: 'commandRun', listener: (message: KlasaMessage, command: Command, params: any[], response: any) => void): this; off(event: 'commandSuccess', listener: (message: KlasaMessage, command: Command, params: any[], response: any) => void): this; off(event: 'commandUnknown', listener: (message: KlasaMessage, command: string, prefix: RegExp, prefixLength: number) => void): this; - off(event: 'finalizerError', listener: (message: KlasaMessage, response: KlasaMessage, runTime: Timestamp, finalizer: Finalizer, error: Error | string) => void): this; + off(event: 'finalizerError', listener: (message: KlasaMessage, command: Command, response: KlasaMessage, runTime: Stopwatch, finalizer: Finalizer, error: Error | string) => void): this; + off(event: 'klasaReady', listener: () => void): this; off(event: 'log', listener: (data: any) => void): this; off(event: 'monitorError', listener: (message: KlasaMessage, monitor: Monitor, error: Error | string) => void): this; off(event: 'pieceDisabled', listener: (piece: Piece) => void): this; @@ -1745,6 +1728,7 @@ declare module 'discord.js' { guildSettings: Settings; language: Language; command: Command | null; + commandText: string | null; prefix: RegExp | null; prefixLength: number | null; readonly responses: KlasaMessage[]; @@ -1753,8 +1737,14 @@ declare module 'discord.js' { readonly flagArgs: Record; readonly reprompted: boolean; readonly reactable: boolean; - send(content?: StringResolvable, options?: MessageOptions): Promise; - prompt(text: string, time?: number): Promise; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; + edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise; usableCommands(): Promise>; hasAtLeastPermissionLevel(min: number): Promise; } @@ -1768,16 +1758,33 @@ declare module 'discord.js' { export interface DMChannel extends SendAliases, ChannelExtendables { } interface PartialSendAliases { - sendLocale(key: string, options?: MessageOptions): Promise; - sendLocale(key: string, localeArgs?: Array, options?: MessageOptions): Promise; - sendMessage(content?: StringResolvable, options?: MessageOptions): Promise; - sendEmbed(embed: MessageEmbed, content?: StringResolvable, options?: MessageOptions): Promise; - sendCode(language: string, content: StringResolvable, options?: MessageOptions): Promise; + sendLocale(key: string, options?: MessageOptions | MessageAdditions): Promise; + sendLocale(key: string, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendLocale(key: string, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + sendLocale(key: string, localeArgs?: Array, options?: MessageOptions | MessageAdditions): Promise; + sendLocale(key: string, localeArgs?: Array, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendLocale(key: string, localeArgs?: Array, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + sendMessage(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + sendMessage(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendMessage(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + sendMessage(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + sendMessage(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + sendMessage(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + sendEmbed(embed: MessageEmbed, content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + sendEmbed(embed: MessageEmbed, content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendEmbed(embed: MessageEmbed, content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + sendCode(language: string, content: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + sendCode(language: string, content: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendCode(language: string, content: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; } interface SendAliases extends PartialSendAliases { - sendFile(attachment: BufferResolvable, name?: string, content?: StringResolvable, options?: MessageOptions): Promise; - sendFiles(attachments: MessageAttachment[], content: StringResolvable, options?: MessageOptions): Promise; + sendFile(attachment: BufferResolvable, name?: string, content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + sendFile(attachment: BufferResolvable, name?: string, content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendFile(attachment: BufferResolvable, name?: string, content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + sendFiles(attachments: MessageAttachment[], content: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + sendFiles(attachments: MessageAttachment[], content: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + sendFiles(attachments: MessageAttachment[], content: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; } interface ChannelExtendables {