From 6bc7f1b0100e239187728335916caef355a3e775 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 9 Jan 2022 01:51:07 +0200 Subject: [PATCH] feat(Interactions): Interaction Handlers, Application Commands and much much more! (#293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot Co-authored-by: Jeroen Claassens Co-authored-by: Antonio Román --- package.json | 18 +- src/errorListeners/CoreCommandError.ts | 14 - src/index.ts | 28 +- src/lib/SapphireClient.ts | 32 +- src/lib/errors/Identifiers.ts | 6 +- src/lib/parsers/Args.ts | 8 +- src/lib/parsers/Maybe.ts | 2 + src/lib/structures/Argument.ts | 6 +- src/lib/structures/Command.ts | 319 +++- src/lib/structures/CommandStore.ts | 61 + src/lib/structures/InteractionHandler.ts | 109 ++ src/lib/structures/InteractionHandlerStore.ts | 84 ++ src/lib/structures/Listener.ts | 8 +- src/lib/structures/Precondition.ts | 40 +- src/lib/structures/PreconditionStore.ts | 57 +- src/lib/types/Enums.ts | 10 + src/lib/types/Events.ts | 307 +++- .../ApplicationCommandRegistries.ts | 61 + .../ApplicationCommandRegistry.ts | 465 ++++++ .../computeDifferences.ts | 434 ++++++ .../application-commands/emitRegistryError.ts | 22 + .../getNeededParameters.ts | 34 + .../application-commands/normalizeInputs.ts | 134 ++ src/lib/utils/logger/Logger.ts | 2 +- .../preconditions/IPreconditionContainer.ts | 20 +- .../PreconditionContainerArray.ts | 38 +- .../PreconditionContainerSingle.ts | 47 +- .../conditions/IPreconditionCondition.ts | 72 +- .../conditions/PreconditionConditionAnd.ts | 38 +- .../conditions/PreconditionConditionOr.ts | 50 +- src/listeners/CoreInteractionCreate.ts | 24 + src/listeners/CoreReady.ts | 13 +- .../CorePossibleAutocompleteInteraction.ts | 41 + .../CoreChatInputCommandAccepted.ts | 26 + .../CorePossibleChatInputCommand.ts | 40 + .../chat-input/CorePreChatInputCommandRun.ts | 29 + .../CoreContextMenuCommandAccepted.ts | 26 + .../CorePossibleContextMenuCommand.ts | 40 + .../CorePreContextMenuCommandRun.ts | 29 + .../command-handler/CoreCommandAccepted.ts | 26 - .../command-handler/CoreCommandTyping.ts | 22 - .../command-handler/CorePreCommandRun.ts | 29 - .../CoreChatInputCommandError.ts | 14 + ...eCommandApplicationCommandRegistryError.ts | 18 + ...CoreCommandAutocompleteInteractionError.ts | 17 + .../CoreContextMenuCommandError.ts | 14 + .../CoreInteractionHandlerError.ts | 17 + .../CoreInteractionHandlerParseError.ts | 17 + .../error-listeners/CoreListenerError.ts} | 8 +- .../CoreMessageCommandError.ts | 14 + .../CoreMessageCommandAccepted.ts | 26 + .../CoreMessageCommandTyping.ts | 22 + .../CoreMessageCreate.ts} | 0 .../CorePreMessageCommandRun.ts | 29 + .../CorePreMessageParser.ts} | 14 +- .../CorePrefixedMessage.ts | 20 +- src/preconditions/ClientPermissions.ts | 53 +- src/preconditions/Cooldown.ts | 55 +- src/preconditions/DMOnly.ts | 22 +- src/preconditions/Enabled.ts | 24 +- src/preconditions/GuildNewsOnly.ts | 34 +- src/preconditions/GuildNewsThreadOnly.ts | 32 +- src/preconditions/GuildOnly.ts | 24 +- src/preconditions/GuildPrivateThreadOnly.ts | 30 +- src/preconditions/GuildPublicThreadOnly.ts | 32 +- src/preconditions/GuildTextOnly.ts | 37 +- src/preconditions/GuildThreadOnly.ts | 35 +- src/preconditions/NSFW.ts | 32 +- src/preconditions/UserPermissions.ts | 41 +- src/preconditions/index.ts | 4 +- .../computeDifferences.test.ts | 1341 +++++++++++++++++ yarn.lock | 1301 +++++++--------- 72 files changed, 5126 insertions(+), 1072 deletions(-) delete mode 100644 src/errorListeners/CoreCommandError.ts create mode 100644 src/lib/structures/InteractionHandler.ts create mode 100644 src/lib/structures/InteractionHandlerStore.ts create mode 100644 src/lib/utils/application-commands/ApplicationCommandRegistries.ts create mode 100644 src/lib/utils/application-commands/ApplicationCommandRegistry.ts create mode 100644 src/lib/utils/application-commands/computeDifferences.ts create mode 100644 src/lib/utils/application-commands/emitRegistryError.ts create mode 100644 src/lib/utils/application-commands/getNeededParameters.ts create mode 100644 src/lib/utils/application-commands/normalizeInputs.ts create mode 100644 src/listeners/CoreInteractionCreate.ts create mode 100644 src/listeners/application-commands/CorePossibleAutocompleteInteraction.ts create mode 100644 src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts create mode 100644 src/listeners/application-commands/chat-input/CorePossibleChatInputCommand.ts create mode 100644 src/listeners/application-commands/chat-input/CorePreChatInputCommandRun.ts create mode 100644 src/listeners/application-commands/context-menu/CoreContextMenuCommandAccepted.ts create mode 100644 src/listeners/application-commands/context-menu/CorePossibleContextMenuCommand.ts create mode 100644 src/listeners/application-commands/context-menu/CorePreContextMenuCommandRun.ts delete mode 100644 src/listeners/command-handler/CoreCommandAccepted.ts delete mode 100644 src/listeners/command-handler/CoreCommandTyping.ts delete mode 100644 src/listeners/command-handler/CorePreCommandRun.ts create mode 100644 src/optional-listeners/error-listeners/CoreChatInputCommandError.ts create mode 100644 src/optional-listeners/error-listeners/CoreCommandApplicationCommandRegistryError.ts create mode 100644 src/optional-listeners/error-listeners/CoreCommandAutocompleteInteractionError.ts create mode 100644 src/optional-listeners/error-listeners/CoreContextMenuCommandError.ts create mode 100644 src/optional-listeners/error-listeners/CoreInteractionHandlerError.ts create mode 100644 src/optional-listeners/error-listeners/CoreInteractionHandlerParseError.ts rename src/{errorListeners/CoreEventError.ts => optional-listeners/error-listeners/CoreListenerError.ts} (56%) create mode 100644 src/optional-listeners/error-listeners/CoreMessageCommandError.ts create mode 100644 src/optional-listeners/message-command-listeners/CoreMessageCommandAccepted.ts create mode 100644 src/optional-listeners/message-command-listeners/CoreMessageCommandTyping.ts rename src/{listeners/command-handler/CoreMessage.ts => optional-listeners/message-command-listeners/CoreMessageCreate.ts} (100%) create mode 100644 src/optional-listeners/message-command-listeners/CorePreMessageCommandRun.ts rename src/{listeners/command-handler/CoreMessageParser.ts => optional-listeners/message-command-listeners/CorePreMessageParser.ts} (90%) rename src/{listeners/command-handler => optional-listeners/message-command-listeners}/CorePrefixedMessage.ts (64%) create mode 100644 tests/application-commands/computeDifferences.test.ts diff --git a/package.json b/package.json index 7d558e911..7144aecc6 100644 --- a/package.json +++ b/package.json @@ -39,28 +39,28 @@ "@commitlint/cli": "^16.0.1", "@commitlint/config-conventional": "^16.0.0", "@favware/npm-deprecate": "^1.0.4", - "@favware/rollup-type-bundler": "^1.0.6", + "@favware/rollup-type-bundler": "^1.0.7", "@sapphire/eslint-config": "^4.0.8", "@sapphire/prettier-config": "^1.2.7", "@sapphire/ts-config": "^3.1.6", "@types/jest": "^27.4.0", - "@types/node": "^17.0.4", + "@types/node": "^17.0.8", "@types/ws": "^8.2.2", - "@typescript-eslint/eslint-plugin": "^5.8.1", - "@typescript-eslint/parser": "^5.8.1", + "@typescript-eslint/eslint-plugin": "^5.9.0", + "@typescript-eslint/parser": "^5.9.0", "cz-conventional-changelog": "^3.3.0", - "discord.js": "^13.5.0", + "discord.js": "^13.5.1", "eslint": "^8.6.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "gen-esm-wrapper": "^1.1.3", "husky": "^7.0.4", - "jest": "^27.4.5", - "jest-circus": "^27.4.5", - "lint-staged": "^12.1.4", + "jest": "^27.4.7", + "jest-circus": "^27.4.6", + "lint-staged": "^12.1.7", "prettier": "^2.5.1", "pretty-quick": "^3.1.3", - "rollup": "^2.62.0", + "rollup": "^2.63.0", "rollup-plugin-version-injector": "^1.3.3", "standard-version": "^9.3.2", "ts-jest": "^27.1.2", diff --git a/src/errorListeners/CoreCommandError.ts b/src/errorListeners/CoreCommandError.ts deleted file mode 100644 index 7865acc0f..000000000 --- a/src/errorListeners/CoreCommandError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PieceContext } from '@sapphire/pieces'; -import { Listener } from '../lib/structures/Listener'; -import { CommandErrorPayload, Events } from '../lib/types/Events'; - -export class CoreEvent extends Listener { - public constructor(context: PieceContext) { - super(context, { event: Events.CommandError }); - } - - public run(error: unknown, context: CommandErrorPayload) { - const { name, location } = context.piece; - this.container.logger.error(`Encountered error on command "${name}" at path "${location.full}"`, error); - } -} diff --git a/src/index.ts b/src/index.ts index 0a5aaeb51..66e90d83b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,20 @@ +import { + acquire, + getDefaultBehaviorWhenNotIdentical, + registries, + setDefaultBehaviorWhenNotIdentical +} from './lib/utils/application-commands/ApplicationCommandRegistries'; +import type { ApplicationCommandRegistry } from './lib/utils/application-commands/ApplicationCommandRegistry'; + +const ApplicationCommandRegistries = { + acquire, + setDefaultBehaviorWhenNotIdentical, + getDefaultBehaviorWhenNotIdentical, + get registries(): ReadonlyMap { + return registries; + } +}; + export { AliasPiece, AliasPieceOptions, @@ -31,12 +48,16 @@ export * from './lib/structures/ArgumentStore'; export * from './lib/structures/Command'; export * from './lib/structures/CommandStore'; export * from './lib/structures/ExtendedArgument'; +export * from './lib/structures/InteractionHandler'; +export * from './lib/structures/InteractionHandlerStore'; export * from './lib/structures/Listener'; export * from './lib/structures/ListenerStore'; export * from './lib/structures/Precondition'; export * from './lib/structures/PreconditionStore'; export * from './lib/types/Enums'; export * from './lib/types/Events'; +export { ApplicationCommandRegistries }; +export { ApplicationCommandRegistry, ApplicationCommandRegistryRegisterOptions } from './lib/utils/application-commands/ApplicationCommandRegistry'; export * from './lib/utils/logger/ILogger'; export * from './lib/utils/logger/Logger'; export * from './lib/utils/preconditions/conditions/IPreconditionCondition'; @@ -47,12 +68,7 @@ export * from './lib/utils/preconditions/containers/UserPermissionsPrecondition' export * from './lib/utils/preconditions/IPreconditionContainer'; export * from './lib/utils/preconditions/PreconditionContainerArray'; export * from './lib/utils/preconditions/PreconditionContainerSingle'; -export * as CorePreconditions from './preconditions'; -/** - * @deprecated. Please use `CorePreconditions.ClientPermissions`. `ClientPermissionsCorePrecondition` will be removed in v3.0.0 - */ -export { CorePrecondition as ClientPermissionsCorePrecondition } from './preconditions/ClientPermissions'; -export type { CooldownContext } from './preconditions/Cooldown'; +export * as CorePreconditions from './preconditions/index'; /** * The [@sapphire/framework](https://github.com/sapphiredev/framework) version that you are currently using. diff --git a/src/lib/SapphireClient.ts b/src/lib/SapphireClient.ts index c1edaa34a..ef5ba8913 100644 --- a/src/lib/SapphireClient.ts +++ b/src/lib/SapphireClient.ts @@ -6,13 +6,17 @@ import type { Plugin } from './plugins/Plugin'; import { PluginManager } from './plugins/PluginManager'; import { ArgumentStore } from './structures/ArgumentStore'; import { CommandStore } from './structures/CommandStore'; +import { InteractionHandlerStore } from './structures/InteractionHandlerStore'; import { ListenerStore } from './structures/ListenerStore'; import { PreconditionStore } from './structures/PreconditionStore'; import { BucketScope, PluginHook } from './types/Enums'; import { Events } from './types/Events'; +import { acquire } from './utils/application-commands/ApplicationCommandRegistries'; import { ILogger, LogLevel } from './utils/logger/ILogger'; import { Logger } from './utils/logger/Logger'; +container.applicationCommandRegistries = { acquire }; + /** * A valid prefix in Sapphire. * * `string`: a single prefix, e.g. `'!'`. @@ -103,12 +107,19 @@ export interface SapphireClientOptions { enableLoaderTraceLoggings?: boolean; /** - * If Sapphire should load our pre-included error event listeners that log any encountered errors to the {@link SapphireClient.logger} instance + * If Sapphire should load the pre-included error event listeners that log any encountered errors to the {@link SapphireClient.logger} instance * @since 1.0.0 * @default true */ loadDefaultErrorListeners?: boolean; + /** + * If Sapphire should load the pre-included message command listeners that are used to process incoming messages for commands. + * @since 3.0.0 + * @default false + */ + loadMessageCommandListeners?: boolean; + /** * Controls whether the bot will automatically appear to be typing when a command is accepted. * @default false @@ -180,7 +191,7 @@ export class SapphireClient extends Client extends Client(x: unknown): x is Maybe; export function isMaybe(x: Maybe | unknown): x is Maybe { return typeof x === 'object' && x !== null && typeof Reflect.get(x, 'exists') === 'boolean'; } + +export type UnwrapMaybeValue> = T extends Some ? V : never; diff --git a/src/lib/structures/Argument.ts b/src/lib/structures/Argument.ts index 877e83c70..293ad30f7 100644 --- a/src/lib/structures/Argument.ts +++ b/src/lib/structures/Argument.ts @@ -5,7 +5,7 @@ import type { ArgumentError } from '../errors/ArgumentError'; import type { UserError } from '../errors/UserError'; import { Args } from '../parsers/Args'; import type { Result } from '../parsers/Result'; -import type { Command } from './Command'; +import type { MessageCommand } from './Command'; /** * Defines a synchronous result of an {@link Argument}, check {@link Argument.AsyncResult} for the asynchronous version. @@ -115,8 +115,8 @@ export interface ArgumentContext extends Record; args: Args; message: Message; - command: Command; - commandContext: Command.RunContext; + command: MessageCommand; + commandContext: MessageCommand.RunContext; minimum?: number; maximum?: number; inclusive?: boolean; diff --git a/src/lib/structures/Command.ts b/src/lib/structures/Command.ts index 0f6838e75..b4b8619ad 100644 --- a/src/lib/structures/Command.ts +++ b/src/lib/structures/Command.ts @@ -1,13 +1,24 @@ -import { AliasPiece, AliasPieceJSON, PieceContext } from '@sapphire/pieces'; +import { AliasPiece, AliasPieceJSON, AliasStore, PieceContext } from '@sapphire/pieces'; import { Awaitable, isNullish } from '@sapphire/utilities'; -import { Message, PermissionResolvable, Permissions, Snowflake } from 'discord.js'; +import { + AutocompleteInteraction, + CommandInteraction, + ContextMenuInteraction, + Message, + PermissionResolvable, + Permissions, + Snowflake +} from 'discord.js'; import * as Lexure from 'lexure'; import { Args } from '../parsers/Args'; -import { BucketScope } from '../types/Enums'; +import { BucketScope, RegisterBehavior } from '../types/Enums'; +import { acquire, getDefaultBehaviorWhenNotIdentical } from '../utils/application-commands/ApplicationCommandRegistries'; +import type { ApplicationCommandRegistry } from '../utils/application-commands/ApplicationCommandRegistry'; +import { getNeededRegistryParameters } from '../utils/application-commands/getNeededParameters'; import { PreconditionContainerArray, PreconditionEntryResolvable } from '../utils/preconditions/PreconditionContainerArray'; import { FlagStrategyOptions, FlagUnorderedStrategy } from '../utils/strategies/FlagUnorderedStrategy'; -export abstract class Command extends AliasPiece { +export class Command extends AliasPiece { /** * A basic summary about the command * @since 1.0.0 @@ -49,6 +60,12 @@ export abstract class Command { + public messagePreParse(message: Message, parameters: string, context: MessageCommand.RunContext): Awaitable { const parser = new Lexure.Parser(this.lexer.setInput(parameters).lex()).setUnorderedStrategy(this.strategy); const args = new Lexure.Args(parser.parse()); return new Args(message, this as any, args, context) as any; @@ -148,11 +167,38 @@ export abstract class Command; + + /** + * Executes the application command's logic. + * @param interaction The interaction that triggered the command. + */ + public chatInputRun?(interaction: CommandInteraction, context: ChatInputCommand.RunContext): Awaitable; + + /** + * Executes the context menu's logic. + * @param interaction The interaction that triggered the command. + */ + public contextMenuRun?(interaction: ContextMenuInteraction, context: ContextMenuCommand.RunContext): Awaitable; + + /** + * Executes the autocomplete logic. + * + * :::tip + * + * You may use this, or alternatively create an {@link InteractionHandler interaction handler} to handle autocomplete interactions. + * Keep in mind that commands take precedence over interaction handlers. + * + * ::: + * + * @param interaction The interaction that triggered the autocomplete. */ - public abstract messageRun(message: Message, args: T, context: Command.RunContext): Awaitable; + public autocompleteRun?(interaction: AutocompleteInteraction): Awaitable; /** * Defines the JSON.stringify behavior of the command. @@ -166,6 +212,104 @@ export abstract class Command { + if (this.chatInputCommandOptions.register) { + registry.registerChatInputCommand( + (builder) => { + builder.setName(this.name).setDescription(this.description); + + if (Reflect.has(this.chatInputCommandOptions, 'defaultPermission')) { + builder.setDefaultPermission(this.chatInputCommandOptions.defaultPermission!); + } + + return builder; + }, + { + behaviorWhenNotIdentical: this.chatInputCommandOptions.behaviorWhenNotIdentical, + guildIds: this.chatInputCommandOptions.guildIds, + idHints: this.chatInputCommandOptions.idHints, + registerCommandIfMissing: true + } + ); + } + } + + /** + * Type-guard that ensures the command supports message commands by checking if the handler for it is present + */ + public supportsMessageCommands(): this is MessageCommand { + return Reflect.has(this, 'messageRun'); + } + + /** + * Type-guard that ensures the command supports chat input commands by checking if the handler for it is present + */ + public supportsChatInputCommands(): this is ChatInputCommand { + return Reflect.has(this, 'chatInputRun'); + } + + /** + * Type-guard that ensures the command supports context menu commands by checking if the handler for it is present + */ + public supportsContextMenuCommands(): this is ContextMenuCommand { + return Reflect.has(this, 'contextMenuRun'); + } + + /** + * Type-guard that ensures the command supports handling autocompletes by checking if the handler for it is present + */ + public supportsAutocompleteInteractions(): this is AutocompleteCommand { + return Reflect.has(this, 'autocompleteRun'); + } + + public override async reload() { + // Remove the aliases from the command store + const store = this.store as AliasStore; + const registry = this.applicationCommandRegistry; + + for (const nameOrId of registry.chatInputCommands) { + const aliasedPiece = store.aliases.get(nameOrId); + if (aliasedPiece === this) { + store.aliases.delete(nameOrId); + } + } + + for (const nameOrId of registry.contextMenuCommands) { + const aliasedPiece = store.aliases.get(nameOrId); + if (aliasedPiece === this) { + store.aliases.delete(nameOrId); + } + } + + // Reset the registry's contents + registry.chatInputCommands.clear(); + registry.contextMenuCommands.clear(); + registry['apiCalls'].length = 0; + + // Reload the command + await super.reload(); + + // Re-initialize the store and the API data (insert in the store handles the register method) + const { applicationCommands, globalCommands, guildCommands } = await getNeededRegistryParameters(); + + // Handle the API calls + // eslint-disable-next-line @typescript-eslint/dot-notation + await registry['runAPICalls'](applicationCommands, globalCommands, guildCommands); + + // Re-set the aliases + for (const nameOrId of registry.chatInputCommands) { + store.aliases.set(nameOrId, this); + } + + for (const nameOrId of registry.contextMenuCommands) { + store.aliases.set(nameOrId, this); + } + } + /** * Parses the command's options and processes them, calling {@link Command#parseConstructorPreConditionsRunIn}, * {@link Command#parseConstructorPreConditionsNsfw}, @@ -327,14 +471,44 @@ export abstract class Command { - /** - * Executes the command's logic. - * @param message The message that triggered the command. - * @param args The value returned by {@link Command.preParse}, by default an instance of {@link Args}. - * @deprecated Use `messageRun` instead. - */ - run?(message: Message, args: T, context: Command.RunContext): Awaitable; +export type MessageCommand = Command & Required>; + +export namespace MessageCommand { + export type Options = CommandOptions; + export type JSON = CommandJSON; + export type Context = AliasPiece.Context; + export type RunInTypes = CommandOptionsRunType; + export type RunContext = MessageCommandContext; +} + +export type ChatInputCommand = Command & Required>; + +export namespace ChatInputCommand { + export type Options = CommandOptions; + export type JSON = CommandJSON; + export type Context = AliasPiece.Context; + export type RunInTypes = CommandOptionsRunType; + export type RunContext = ChatInputCommandContext; +} + +export type ContextMenuCommand = Command & Required>; + +export namespace ContextMenuCommand { + export type Options = CommandOptions; + export type JSON = CommandJSON; + export type Context = AliasPiece.Context; + export type RunInTypes = CommandOptionsRunType; + export type RunContext = ContextMenuCommandContext; +} + +export type AutocompleteCommand = Command & Required>; + +export namespace AutocompleteCommand { + export type Options = CommandOptions; + export type JSON = CommandJSON; + export type Context = AliasPiece.Context; + export type RunInTypes = CommandOptionsRunType; + export type RunContext = AutocompleteCommandContext; } /** @@ -508,9 +682,62 @@ export interface CommandOptions extends AliasPiece.Options, FlagStrategyOptions * @default true */ typing?: boolean; + /** + * Shortcuts for registering simple chat input commands + * + * :::warn + * + * You should only use this if your command does not take in options, and is just a chat input one. + * Otherwise, please read the [guide about registering application commands](https://www.sapphirejs.dev/docs/Guide/commands/registering-application-commands) instead. + * + * ::: + * + * @since 3.0.0 + */ + chatInputCommand?: CommandChatInputRegisterShortcut; +} + +export interface CommandChatInputRegisterShortcut { + /** + * Specifies what we should do when the command is present, but not identical with the data you provided + * @default RegisterBehavior.LogToConsole + */ + behaviorWhenNotIdentical?: RegisterBehavior; + /** + * If we should register the command, be it missing or present already + * @default false + */ + register: boolean; + /** + * If this is specified, the application commands will only be registered for these guild ids. + * + * :::tip + * + * If you want to register both guild and global chat input commands, + * please read the [guide about registering application commands](https://www.sapphirejs.dev/docs/Guide/commands/registering-application-commands) instead. + * + * ::: + * + */ + guildIds?: string[]; + /** + * Specifies a list of command ids that we should check in the event of a name mismatch + * @default [] + */ + idHints?: string[]; + /** + * Sets the `defaultPermission` field for the chat input command + * + * :::warn + * + * This will be deprecated in the future for Discord's new permission system + * + * ::: + */ + defaultPermission?: boolean; } -export interface CommandContext extends Record { +export interface MessageCommandContext extends Record { /** * The prefix used to run this command. * @@ -522,12 +749,45 @@ export interface CommandContext extends Record { */ commandName: string; /** - * The matched prefix, this will always be the same as {@link Command.RunContext.prefix} if it was a string, otherwise it is + * The matched prefix, this will always be the same as {@link MessageCommand.RunContext.prefix} if it was a string, otherwise it is * the result of doing `prefix.exec(content)[0]`. */ commandPrefix: string; } +export interface ChatInputCommandContext extends Record { + /** + * The name of the command. + */ + commandName: string; + /** + * The id of the command. + */ + commandId: string; +} + +export interface ContextMenuCommandContext extends Record { + /** + * The name of the command. + */ + commandName: string; + /** + * The id of the command. + */ + commandId: string; +} + +export interface AutocompleteCommandContext extends Record { + /** + * The name of the command. + */ + commandName: string; + /** + * The id of the command. + */ + commandId: string; +} + export interface CommandJSON extends AliasPieceJSON { description: string; detailedDescription: string; @@ -538,6 +798,5 @@ export namespace Command { export type Options = CommandOptions; export type JSON = CommandJSON; export type Context = AliasPiece.Context; - export type RunContext = CommandContext; export type RunInTypes = CommandOptionsRunType; } diff --git a/src/lib/structures/CommandStore.ts b/src/lib/structures/CommandStore.ts index f2af5f6f4..4898d4aa1 100644 --- a/src/lib/structures/CommandStore.ts +++ b/src/lib/structures/CommandStore.ts @@ -1,4 +1,6 @@ import { AliasStore } from '@sapphire/pieces'; +import { emitRegistryError } from '../utils/application-commands/emitRegistryError'; +import { getNeededRegistryParameters } from '../utils/application-commands/getNeededParameters'; import { Command } from './Command'; /** @@ -18,4 +20,63 @@ export class CommandStore extends AliasStore { categories.delete(null); return [...categories] as string[]; } + + public override async insert(piece: Command) { + try { + await piece.registerApplicationCommands(piece.applicationCommandRegistry); + } catch (error) { + emitRegistryError(error, piece); + } + + return super.insert(piece); + } + + public override unload(name: string | Command) { + const piece = this.resolve(name); + + // Remove the aliases from the store + for (const nameOrId of piece.applicationCommandRegistry.chatInputCommands) { + const aliasedPiece = this.aliases.get(nameOrId); + if (aliasedPiece === piece) { + this.aliases.delete(nameOrId); + } + } + + for (const nameOrId of piece.applicationCommandRegistry.contextMenuCommands) { + const aliasedPiece = this.aliases.get(nameOrId); + if (aliasedPiece === piece) { + this.aliases.delete(nameOrId); + } + } + + // Reset the registry's contents + piece.applicationCommandRegistry.chatInputCommands.clear(); + piece.applicationCommandRegistry.contextMenuCommands.clear(); + piece.applicationCommandRegistry['apiCalls'].length = 0; + + return super.unload(name); + } + + public override async loadAll() { + await super.loadAll(); + + // If we don't have an application, that means this was called on login... + if (!this.container.client.application) return; + + const { applicationCommands, globalCommands, guildCommands } = await getNeededRegistryParameters(); + + for (const command of this.values()) { + // eslint-disable-next-line @typescript-eslint/dot-notation + await command.applicationCommandRegistry['runAPICalls'](applicationCommands, globalCommands, guildCommands); + + // Reinitialize the aliases + for (const nameOrId of command.applicationCommandRegistry.chatInputCommands) { + this.aliases.set(nameOrId, command); + } + + for (const nameOrId of command.applicationCommandRegistry.contextMenuCommands) { + this.aliases.set(nameOrId, command); + } + } + } } diff --git a/src/lib/structures/InteractionHandler.ts b/src/lib/structures/InteractionHandler.ts new file mode 100644 index 000000000..c82262bd8 --- /dev/null +++ b/src/lib/structures/InteractionHandler.ts @@ -0,0 +1,109 @@ +import { Piece, PieceContext, PieceJSON, PieceOptions } from '@sapphire/pieces'; +import type { Awaitable } from '@sapphire/utilities'; +import type { Interaction } from 'discord.js'; +import { some, Maybe, none, None, UnwrapMaybeValue } from '../parsers/Maybe'; + +export abstract class InteractionHandler extends Piece { + /** + * The type for this handler + * @since 3.0.0 + */ + public readonly interactionHandlerType: InteractionHandlerTypes; + + public constructor(context: PieceContext, options: InteractionHandlerOptions) { + super(context, options); + + this.interactionHandlerType = options.interactionHandlerType; + } + + public abstract run(interaction: Interaction, parsedData?: unknown): unknown; + + /** + * A custom function that will be called when checking if an interaction should be passed to this handler. + * You can use this method to not only filter by ids, but also pre-parse the data from the id for use in the run method. + * + * By default, all interactions of the type you specified will run in a handler. You should override this method + * to change that behavior. + * + * @example + * ```typescript + * // Parsing a button handler + * public override parse(interaction: ButtonInteraction) { + * if (interaction.customId.startsWith('my-awesome-clicky-button')) { + * // Returning a `some` here means that the run method should be called next! + * return this.some({ isMyBotAwesome: true, awesomenessLevel: 9001 }); + * } + * + * // Returning a `none` means this interaction shouldn't run in this handler + * return this.none(); + * } + * ``` + * + * @example + * ```typescript + * // Getting data from a database based on the custom id + * public override async parse(interaction: ButtonInteraction) { + * // This code is purely for demonstration purposes only! + * if (interaction.customId.startsWith('example-data')) { + * const [, userId, channelId] = interaction.customId.split('.'); + * + * const dataFromDatabase = await container.prisma.exampleData.findFirst({ where: { userId, channelId } }); + * + * // Returning a `some` here means that the run method should be called next! + * return this.some(dataFromDatabase); + * } + * + * // Returning a `none` means this interaction shouldn't run in this handler + * return this.none(); + * } + * ``` + * + * @returns A {@link Maybe} (or a {@link Promise Promised} {@link Maybe}) that indicates if this interaction should be + * handled by this handler, and any extra data that should be passed to the {@link InteractionHandler.run run method} + */ + public parse(_interaction: Interaction): Awaitable> { + return this.some(); + } + + public some(): Maybe; + public some(data: T): Maybe; + public some(data?: T): Maybe { + return some(data); + } + + public none(): None { + return none(); + } + + public toJSON(): InteractionHandlerJSON { + return { + ...super.toJSON(), + interactionHandlerType: this.interactionHandlerType + }; + } +} + +export interface InteractionHandlerOptions extends PieceOptions { + readonly interactionHandlerType: InteractionHandlerTypes; +} + +export interface InteractionHandlerJSON extends PieceJSON { + interactionHandlerType: InteractionHandlerTypes; +} + +export namespace InteractionHandler { + export type Options = InteractionHandlerOptions; + export type JSON = InteractionHandlerJSON; + export type ParseResult = UnwrapMaybeValue>>; +} + +export const enum InteractionHandlerTypes { + // Specifically focused types + Button = 'BUTTON', + SelectMenu = 'SELECT_MENU', + + // More free-falling handlers, for 1 shared handler between buttons and select menus (someone will have a use for this >,>) + MessageComponent = 'MESSAGE_COMPONENT', + // Optional autocompletes, you can use this or in-command + Autocomplete = 'AUTOCOMPLETE' +} diff --git a/src/lib/structures/InteractionHandlerStore.ts b/src/lib/structures/InteractionHandlerStore.ts new file mode 100644 index 000000000..bc5777573 --- /dev/null +++ b/src/lib/structures/InteractionHandlerStore.ts @@ -0,0 +1,84 @@ +import { Store } from '@sapphire/pieces'; +import type { Interaction } from 'discord.js'; +import { isSome } from '../parsers/Maybe'; +import { err, fromAsync, isErr, Result } from '../parsers/Result'; +import { Events } from '../types/Events'; +import { InteractionHandler, InteractionHandlerTypes } from './InteractionHandler'; + +export class InteractionHandlerStore extends Store { + public constructor() { + super(InteractionHandler as any, { name: 'interaction-handlers' }); + } + + public async run(interaction: Interaction) { + // Early-exit for optimization + if (this.size === 0) return false; + + const promises = []; + + // Iterate through every registered handler + for (const handler of this.values()) { + const filter = InteractionHandlerFilters.get(handler.interactionHandlerType); + + // If the filter is missing (we don't support it or someone didn't register it manually while waiting for us to implement it), + // or it doesn't match the expected handler type, skip the handler + if (!filter?.(interaction)) continue; + + // Get the result of the `parse` method in the handler + const result = await fromAsync(() => handler.parse(interaction)); + + if (isErr(result)) { + // If the `parse` method threw an error (spoiler: please don't), skip the handler + this.container.client.emit(Events.InteractionHandlerParseError, result.error, { interaction, handler }); + continue; + } + + const finalValue = result.value; + + // If the `parse` method returned a `Some` (whatever that `Some`'s value is, it should be handled) + if (isSome(finalValue)) { + // Schedule the run of the handler method + const promise = fromAsync(() => handler.run(interaction, finalValue.value)).then((res) => { + return isErr(res) ? err({ handler, error: res.error }) : res; + }); + + promises.push(promise); + } + } + + // Yet another early exit + if (promises.length === 0) return false; + + const results = await Promise.allSettled(promises); + + for (const result of results) { + const res = ( + result as PromiseFulfilledResult< + Result< + unknown, + { + error: Error; + handler: InteractionHandler; + } + > + > + ).value; + + if (!isErr(res)) continue; + + const value = res.error; + + this.container.client.emit(Events.InteractionHandlerError, value.error, { interaction, handler: value.handler }); + } + + return true; + } +} + +export const InteractionHandlerFilters = new Map boolean>([ + [InteractionHandlerTypes.Button, (interaction) => interaction.isButton()], + [InteractionHandlerTypes.SelectMenu, (interaction) => interaction.isSelectMenu()], + + [InteractionHandlerTypes.MessageComponent, (interaction) => interaction.isMessageComponent()], + [InteractionHandlerTypes.Autocomplete, (Interaction) => Interaction.isAutocomplete()] +]); diff --git a/src/lib/structures/Listener.ts b/src/lib/structures/Listener.ts index eeb46c5fe..f337af99f 100644 --- a/src/lib/structures/Listener.ts +++ b/src/lib/structures/Listener.ts @@ -54,7 +54,7 @@ export abstract class Listener void) | null; - public constructor(context: Listener.Context, options: Listener.Options = {}) { + public constructor(context: Listener.Context, options: O = {} as O) { super(context, options); this.emitter = @@ -135,12 +135,12 @@ export abstract class Listener>; export type AsyncPreconditionResult = Promise>; -export abstract class Precondition extends Piece { +export class Precondition extends Piece { public readonly position: number | null; public constructor(context: Piece.Context, options: Precondition.Options = {}) { @@ -18,7 +18,11 @@ export abstract class Precondition = {}): Precondition.Result { return err(new PreconditionError({ precondition: this, ...options })); } + + protected async fetchChannelFromInteraction(interaction: BaseCommandInteraction) { + const channel = (await interaction.client.channels.fetch(interaction.channelId, { + cache: false, + allowUnknownGuild: true + })) as TextBasedChannel; + + return channel; + } +} + +export abstract class AllFlowsPrecondition extends Precondition { + public abstract messageRun(message: Message, command: MessageCommand, context: Precondition.Context): Precondition.Result; + + public abstract chatInputRun(interaction: CommandInteraction, command: ChatInputCommand, context: Precondition.Context): Precondition.Result; + + public abstract contextMenuRun( + interaction: ContextMenuInteraction, + command: ContextMenuCommand, + context: Precondition.Context + ): Precondition.Result; } /** @@ -124,3 +149,10 @@ export namespace Precondition { export type Result = PreconditionResult; export type AsyncResult = AsyncPreconditionResult; } + +export namespace AllFlowsPrecondition { + export type Options = PreconditionOptions; + export type Context = PreconditionContext; + export type Result = PreconditionResult; + export type AsyncResult = AsyncPreconditionResult; +} diff --git a/src/lib/structures/PreconditionStore.ts b/src/lib/structures/PreconditionStore.ts index 661734cdf..686b1a507 100644 --- a/src/lib/structures/PreconditionStore.ts +++ b/src/lib/structures/PreconditionStore.ts @@ -1,7 +1,8 @@ import { Store } from '@sapphire/pieces'; -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; +import { Identifiers } from '../errors/Identifiers'; import { ok } from '../parsers/Result'; -import type { Command } from './Command'; +import type { ChatInputCommand, ContextMenuCommand, MessageCommand } from './Command'; import { AsyncPreconditionResult, Precondition, PreconditionContext } from './Precondition'; export class PreconditionStore extends Store { @@ -11,9 +12,53 @@ export class PreconditionStore extends Store { super(Precondition as any, { name: 'preconditions' }); } - public async run(message: Message, command: Command, context: PreconditionContext = {}): AsyncPreconditionResult { + public async messageRun(message: Message, command: MessageCommand, context: PreconditionContext = {}): AsyncPreconditionResult { for (const precondition of this.globalPreconditions) { - const result = await precondition.run(message, command, context); + const result = precondition.messageRun + ? await precondition.messageRun(message, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingMessageHandler, + message: `The precondition "${precondition.name}" is missing a "messageRun" handler, but it was requested for the "${command.name}" command.` + }); + + if (!result.success) return result; + } + + return ok(); + } + + public async chatInputRun( + interaction: CommandInteraction, + command: ChatInputCommand, + context: PreconditionContext = {} + ): AsyncPreconditionResult { + for (const precondition of this.globalPreconditions) { + const result = precondition.chatInputRun + ? await precondition.chatInputRun(interaction, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingChatInputHandler, + message: `The precondition "${precondition.name}" is missing a "chatInputRun" handler, but it was requested for the "${command.name}" command.` + }); + + if (!result.success) return result; + } + + return ok(); + } + + public async contextMenuRun( + interaction: ContextMenuInteraction, + command: ContextMenuCommand, + context: PreconditionContext = {} + ): AsyncPreconditionResult { + for (const precondition of this.globalPreconditions) { + const result = precondition.contextMenuRun + ? await precondition.contextMenuRun(interaction, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingContextMenuHandler, + message: `The precondition "${precondition.name}" is missing a "contextMenuRun" handler, but it was requested for the "${command.name}" command.` + }); + if (!result.success) return result; } @@ -24,7 +69,7 @@ export class PreconditionStore extends Store { if (value.position !== null) { const index = this.globalPreconditions.findIndex((precondition) => precondition.position! >= value.position!); - // If a middleware with lower priority wasn't found, push to the end of the array + // If a precondition with lower priority wasn't found, push to the end of the array if (index === -1) this.globalPreconditions.push(value); else this.globalPreconditions.splice(index, 0, value); } @@ -35,7 +80,7 @@ export class PreconditionStore extends Store { public delete(key: string): boolean { const index = this.globalPreconditions.findIndex((precondition) => precondition.name === key); - // If the middleware was found, remove it + // If the precondition was found, remove it if (index !== -1) this.globalPreconditions.splice(index, 1); return super.delete(key); diff --git a/src/lib/types/Enums.ts b/src/lib/types/Enums.ts index 958081e95..8a5aa896f 100644 --- a/src/lib/types/Enums.ts +++ b/src/lib/types/Enums.ts @@ -33,3 +33,13 @@ export const enum BucketScope { */ User } + +export const enum RegisterBehavior { + Overwrite = 'OVERWRITE', + LogToConsole = 'LOG_TO_CONSOLE' +} + +export const enum InternalRegistryAPIType { + ChatInput, + ContextMenu +} diff --git a/src/lib/types/Events.ts b/src/lib/types/Events.ts index 7b75429c0..91fe36a63 100644 --- a/src/lib/types/Events.ts +++ b/src/lib/types/Events.ts @@ -1,8 +1,17 @@ import type { Piece, Store } from '@sapphire/pieces'; -import { Constants, Message } from 'discord.js'; +import { AutocompleteInteraction, CommandInteraction, Constants, ContextMenuInteraction, Interaction, Message } from 'discord.js'; import type { UserError } from '../errors/UserError'; -import type { Args } from '../parsers/Args'; -import type { Command } from '../structures/Command'; +import type { + AutocompleteCommand, + AutocompleteCommandContext, + ChatInputCommand, + ChatInputCommandContext, + Command, + ContextMenuCommand, + ContextMenuCommandContext, + MessageCommand +} from '../structures/Command'; +import type { InteractionHandler } from '../structures/InteractionHandler'; import type { Listener } from '../structures/Listener'; import type { PluginHook } from './Enums'; @@ -26,22 +35,28 @@ export const Events = { GuildMemberAdd: Constants.Events.GUILD_MEMBER_ADD, GuildMemberAvailable: Constants.Events.GUILD_MEMBER_AVAILABLE, GuildMemberRemove: Constants.Events.GUILD_MEMBER_REMOVE, - GuildMemberUpdate: Constants.Events.GUILD_MEMBER_UPDATE, GuildMembersChunk: Constants.Events.GUILD_MEMBERS_CHUNK, + GuildMemberUpdate: Constants.Events.GUILD_MEMBER_UPDATE, GuildRoleCreate: Constants.Events.GUILD_ROLE_CREATE, GuildRoleDelete: Constants.Events.GUILD_ROLE_DELETE, GuildRoleUpdate: Constants.Events.GUILD_ROLE_UPDATE, + GuildStickerCreate: Constants.Events.GUILD_STICKER_CREATE, + GuildStickerDelete: Constants.Events.GUILD_STICKER_DELETE, + GuildStickerUpdate: Constants.Events.GUILD_STICKER_UPDATE, GuildUnavailable: Constants.Events.GUILD_UNAVAILABLE, GuildUpdate: Constants.Events.GUILD_UPDATE, + InteractionCreate: Constants.Events.INTERACTION_CREATE, Invalidated: Constants.Events.INVALIDATED, + InvalidRequestWarning: Constants.Events.INVALID_REQUEST_WARNING, InviteCreate: Constants.Events.INVITE_CREATE, InviteDelete: Constants.Events.INVITE_DELETE, MessageBulkDelete: Constants.Events.MESSAGE_BULK_DELETE, MessageCreate: Constants.Events.MESSAGE_CREATE, MessageDelete: Constants.Events.MESSAGE_DELETE, MessageReactionAdd: Constants.Events.MESSAGE_REACTION_ADD, - MessageReactionRemoveAll: Constants.Events.MESSAGE_REACTION_REMOVE_ALL, MessageReactionRemove: Constants.Events.MESSAGE_REACTION_REMOVE, + MessageReactionRemoveAll: Constants.Events.MESSAGE_REACTION_REMOVE_ALL, + MessageReactionRemoveEmoji: Constants.Events.MESSAGE_REACTION_REMOVE_EMOJI, MessageUpdate: Constants.Events.MESSAGE_UPDATE, PresenceUpdate: Constants.Events.PRESENCE_UPDATE, RateLimit: Constants.Events.RATE_LIMIT, @@ -51,33 +66,96 @@ export const Events = { ShardReady: Constants.Events.SHARD_READY, ShardReconnecting: Constants.Events.SHARD_RECONNECTING, ShardResume: Constants.Events.SHARD_RESUME, + StageInstanceCreate: Constants.Events.STAGE_INSTANCE_CREATE, + StageInstanceDelete: Constants.Events.STAGE_INSTANCE_DELETE, + StageInstanceUpdate: Constants.Events.STAGE_INSTANCE_UPDATE, + ThreadCreate: Constants.Events.THREAD_CREATE, + ThreadDelete: Constants.Events.THREAD_DELETE, + ThreadListSync: Constants.Events.THREAD_LIST_SYNC, + ThreadMembersUpdate: Constants.Events.THREAD_MEMBERS_UPDATE, + ThreadMemberUpdate: Constants.Events.THREAD_MEMBER_UPDATE, + ThreadUpdate: Constants.Events.THREAD_UPDATE, TypingStart: Constants.Events.TYPING_START, UserUpdate: Constants.Events.USER_UPDATE, + VoiceServerUpdate: Constants.Events.VOICE_SERVER_UPDATE, VoiceStateUpdate: Constants.Events.VOICE_STATE_UPDATE, Warn: Constants.Events.WARN, WebhooksUpdate: Constants.Events.WEBHOOKS_UPDATE, // #endregion Discord.js base events - // #region Sapphire load cycle events - CommandAccepted: 'commandAccepted' as const, - CommandDenied: 'commandDenied' as const, - CommandError: 'commandError' as const, - CommandFinish: 'commandFinish' as const, - CommandRun: 'commandRun' as const, - CommandSuccess: 'commandSuccess' as const, - CommandTypingError: 'commandTypingError' as const, - ListenerError: 'listenerError' as const, + // #region Sapphire events + // Message commands chain + PreMessageParsed: 'preMessageParsed' as const, MentionPrefixOnly: 'mentionPrefixOnly' as const, NonPrefixedMessage: 'nonPrefixedMessage' as const, + PrefixedMessage: 'prefixedMessage' as const, + + UnknownMessageCommandName: 'unknownMessageCommandName' as const, + UnknownMessageCommand: 'unknownMessageCommand' as const, + CommandDoesNotHaveMessageCommandHandler: 'commandDoesNotHaveMessageCommandHandler' as const, + PreMessageCommandRun: 'preMessageCommandRun' as const, + + MessageCommandDenied: 'messageCommandDenied' as const, + MessageCommandAccepted: 'messageCommandAccepted' as const, + + MessageCommandRun: 'messageCommandRun' as const, + MessageCommandSuccess: 'messageCommandSuccess' as const, + MessageCommandError: 'messageCommandError' as const, + MessageCommandFinish: 'messageCommandFinish' as const, + + MessageCommandTypingError: 'messageCommandTypingError' as const, + + // Listener errors + ListenerError: 'listenerError' as const, + + // Registry errors + CommandApplicationCommandRegistryError: 'commandApplicationCommandRegistryError' as const, + + // Piece store? PiecePostLoad: 'piecePostLoad' as const, PieceUnload: 'pieceUnload' as const, + + // Plugin PluginLoaded: 'pluginLoaded' as const, - PreCommandRun: 'preCommandRun' as const, - PrefixedMessage: 'prefixedMessage' as const, - PreMessageParsed: 'preMessageParsed' as const, - UnknownCommand: 'unknownCommand' as const, - UnknownCommandName: 'unknownCommandName' as const - // #endregion Sapphire load cycle events + + // Interaction handlers + InteractionHandlerParseError: 'interactionHandlerParseError' as const, + InteractionHandlerError: 'interactionHandlerError' as const, + + // Autocomplete interaction + PossibleAutocompleteInteraction: 'possibleAutocompleteInteraction' as const, + CommandAutocompleteInteractionSuccess: 'commandAutocompleteInteractionSuccess' as const, + CommandAutocompleteInteractionError: 'commandAutocompleteInteractionError' as const, + + // Chat input command chain + PossibleChatInputCommand: 'possibleChatInputCommand' as const, + UnknownChatInputCommand: 'unknownChatInputCommand' as const, + CommandDoesNotHaveChatInputCommandHandler: 'commandDoesNotHaveChatInputCommandHandler' as const, + PreChatInputCommandRun: 'preChatInputCommandRun' as const, + + ChatInputCommandDenied: 'chatInputCommandDenied' as const, + ChatInputCommandAccepted: 'chatInputCommandAccepted' as const, + + ChatInputCommandRun: 'chatInputCommandRun' as const, + ChatInputCommandSuccess: 'chatInputCommandSuccess' as const, + ChatInputCommandError: 'chatInputCommandError' as const, + ChatInputCommandFinish: 'chatInputCommandFinish' as const, + + // Context menu chain + PossibleContextMenuCommand: 'possibleContextMenuCommand' as const, + UnknownContextMenuCommand: 'unknownContextMenuCommand' as const, + CommandDoesNotHaveContextMenuCommandHandler: 'commandDoesNotHaveContextMenuCommandHandler' as const, + PreContextMenuCommandRun: 'preContextMenuCommandRun' as const, + + ContextMenuCommandDenied: 'contextMenuCommandDenied' as const, + ContextMenuCommandAccepted: 'contextMenuCommandAccepted' as const, + + ContextMenuCommandRun: 'contextMenuCommandRun' as const, + ContextMenuCommandSuccess: 'contextMenuCommandSuccess' as const, + ContextMenuCommandError: 'contextMenuCommandError' as const, + ContextMenuCommandFinish: 'contextMenuCommandFinish' as const + + // #endregion Sapphire events }; export interface IPieceError { @@ -88,70 +166,203 @@ export interface ListenerErrorPayload extends IPieceError { piece: Listener; } -export interface UnknownCommandNamePayload { +export interface UnknownMessageCommandNamePayload { + message: Message; + prefix: string | RegExp; + commandPrefix: string; +} + +export interface CommandDoesNotHaveMessageCommandHandler { message: Message; prefix: string | RegExp; commandPrefix: string; + command: Command; } -export interface UnknownCommandPayload extends UnknownCommandNamePayload { +export interface UnknownMessageCommandPayload extends UnknownMessageCommandNamePayload { commandName: string; } -export interface ICommandPayload { +export interface IMessageCommandPayload { message: Message; - command: Command; + command: MessageCommand; } -export interface PreCommandRunPayload extends CommandDeniedPayload {} +export interface PreMessageCommandRunPayload extends MessageCommandDeniedPayload {} -export interface CommandDeniedPayload extends ICommandPayload { +export interface MessageCommandDeniedPayload extends IMessageCommandPayload { parameters: string; - context: Command.RunContext; + context: MessageCommand.RunContext; } -export interface CommandAcceptedPayload extends ICommandPayload { +export interface MessageCommandAcceptedPayload extends IMessageCommandPayload { parameters: string; - context: Command.RunContext; + context: MessageCommand.RunContext; } -export interface CommandRunPayload extends CommandAcceptedPayload { - args: T; +export interface MessageCommandRunPayload extends MessageCommandAcceptedPayload { + args: unknown; } -export interface CommandFinishPayload extends CommandRunPayload {} +export interface MessageCommandFinishPayload extends MessageCommandRunPayload {} + +export interface MessageCommandErrorPayload extends MessageCommandRunPayload {} -export interface CommandErrorPayload extends CommandRunPayload { - piece: Command; +export interface MessageCommandSuccessPayload extends MessageCommandRunPayload { + result: unknown; } -export interface CommandSuccessPayload extends CommandRunPayload { +export interface MessageCommandTypingErrorPayload extends MessageCommandRunPayload {} + +export interface UnknownChatInputCommandPayload { + interaction: CommandInteraction; + context: ChatInputCommandContext; +} + +export interface CommandDoesNotHaveChatInputCommandHandlerPayload { + interaction: CommandInteraction; + command: Command; + context: ChatInputCommandContext; +} + +export interface IChatInputCommandPayload { + interaction: CommandInteraction; + command: ChatInputCommand; +} + +export interface PreChatInputCommandRunPayload extends IChatInputCommandPayload { + context: ChatInputCommandContext; +} + +export interface ChatInputCommandDeniedPayload extends IChatInputCommandPayload { + context: ChatInputCommandContext; +} + +export interface ChatInputCommandAcceptedPayload extends PreChatInputCommandRunPayload {} + +export interface ChatInputCommandRunPayload extends ChatInputCommandAcceptedPayload {} + +export interface ChatInputCommandSuccessPayload extends ChatInputCommandRunPayload { result: unknown; } -export interface CommandTypingErrorPayload extends CommandRunPayload {} +export interface ChatInputCommandErrorPayload extends IChatInputCommandPayload {} + +export interface UnknownContextMenuCommandPayload { + interaction: ContextMenuInteraction; + context: ContextMenuCommandContext; +} + +export interface CommandDoesNotHaveContextMenuCommandHandlerPayload { + interaction: ContextMenuInteraction; + context: ContextMenuCommandContext; + command: Command; +} + +export interface IContextMenuCommandPayload { + interaction: ContextMenuInteraction; + command: ContextMenuCommand; +} + +export interface PreContextMenuCommandRunPayload extends IContextMenuCommandPayload { + context: ContextMenuCommandContext; +} + +export interface ContextMenuCommandDeniedPayload extends IContextMenuCommandPayload { + context: ContextMenuCommandContext; +} + +export interface ContextMenuCommandAcceptedPayload extends PreContextMenuCommandRunPayload {} + +export interface ContextMenuCommandRunPayload extends ContextMenuCommandAcceptedPayload {} + +export interface ContextMenuCommandSuccessPayload extends ContextMenuCommandRunPayload { + result: unknown; +} + +export interface ContextMenuCommandErrorPayload extends IContextMenuCommandPayload {} + +export interface IInteractionHandlerPayload { + interaction: Interaction; + handler: InteractionHandler; +} + +export interface InteractionHandlerParseError extends IInteractionHandlerPayload {} + +export interface InteractionHandlerError extends IInteractionHandlerPayload {} + +export interface AutocompleteInteractionPayload { + interaction: AutocompleteInteraction; + command: AutocompleteCommand; + context: AutocompleteCommandContext; +} declare module 'discord.js' { interface ClientEvents { // #region Sapphire load cycle events [Events.PieceUnload]: [store: Store, piece: Piece]; [Events.PiecePostLoad]: [store: Store, piece: Piece]; - [Events.MentionPrefixOnly]: [message: Message]; + [Events.ListenerError]: [error: unknown, payload: ListenerErrorPayload]; + [Events.CommandApplicationCommandRegistryError]: [error: unknown, command: Command]; + [Events.PreMessageParsed]: [message: Message]; + [Events.MentionPrefixOnly]: [message: Message]; + [Events.NonPrefixedMessage]: [message: Message]; [Events.PrefixedMessage]: [message: Message, prefix: string | RegExp]; - [Events.UnknownCommandName]: [payload: UnknownCommandNamePayload]; - [Events.UnknownCommand]: [payload: UnknownCommandPayload]; - [Events.PreCommandRun]: [payload: PreCommandRunPayload]; - [Events.CommandDenied]: [error: UserError, payload: CommandDeniedPayload]; - [Events.CommandAccepted]: [payload: CommandAcceptedPayload]; - [Events.CommandRun]: [message: Message, command: Command, payload: CommandRunPayload]; - [Events.CommandSuccess]: [payload: CommandSuccessPayload]; - [Events.CommandError]: [error: unknown, payload: CommandErrorPayload]; - [Events.CommandFinish]: [message: Message, command: Command, payload: CommandFinishPayload]; - [Events.CommandTypingError]: [error: unknown, payload: CommandTypingErrorPayload]; + + [Events.UnknownMessageCommandName]: [payload: UnknownMessageCommandNamePayload]; + [Events.UnknownMessageCommand]: [payload: UnknownMessageCommandPayload]; + [Events.CommandDoesNotHaveMessageCommandHandler]: [payload: CommandDoesNotHaveMessageCommandHandler]; + [Events.PreMessageCommandRun]: [payload: PreMessageCommandRunPayload]; + + [Events.MessageCommandDenied]: [error: UserError, payload: MessageCommandDeniedPayload]; + [Events.MessageCommandAccepted]: [payload: MessageCommandAcceptedPayload]; + + [Events.MessageCommandRun]: [message: Message, command: Command, payload: MessageCommandRunPayload]; + [Events.MessageCommandSuccess]: [payload: MessageCommandSuccessPayload]; + [Events.MessageCommandError]: [error: unknown, payload: MessageCommandErrorPayload]; + [Events.MessageCommandFinish]: [message: Message, command: Command, payload: MessageCommandFinishPayload]; + + [Events.MessageCommandTypingError]: [error: Error, payload: MessageCommandTypingErrorPayload]; + [Events.PluginLoaded]: [hook: PluginHook, name: string | undefined]; - [Events.NonPrefixedMessage]: [message: Message]; + + [Events.InteractionHandlerParseError]: [error: unknown, payload: InteractionHandlerParseError]; + [Events.InteractionHandlerError]: [error: unknown, payload: InteractionHandlerError]; + + [Events.PossibleAutocompleteInteraction]: [interaction: AutocompleteInteraction]; + [Events.CommandAutocompleteInteractionError]: [error: unknown, payload: AutocompleteInteractionPayload]; + [Events.CommandAutocompleteInteractionSuccess]: [payload: AutocompleteInteractionPayload]; + + // Chat input command chain + [Events.PossibleChatInputCommand]: [interaction: CommandInteraction]; + [Events.UnknownChatInputCommand]: [payload: UnknownChatInputCommandPayload]; + [Events.CommandDoesNotHaveChatInputCommandHandler]: [payload: CommandDoesNotHaveChatInputCommandHandlerPayload]; + [Events.PreChatInputCommandRun]: [payload: PreChatInputCommandRunPayload]; + + [Events.ChatInputCommandDenied]: [error: UserError, payload: ChatInputCommandDeniedPayload]; + [Events.ChatInputCommandAccepted]: [payload: ChatInputCommandAcceptedPayload]; + + [Events.ChatInputCommandRun]: [interaction: CommandInteraction, command: ChatInputCommand, payload: ChatInputCommandRunPayload]; + [Events.ChatInputCommandSuccess]: [payload: ChatInputCommandSuccessPayload]; + [Events.ChatInputCommandError]: [error: unknown, payload: ChatInputCommandErrorPayload]; + [Events.ChatInputCommandFinish]: [interaction: CommandInteraction, command: ChatInputCommand, payload: ChatInputCommandRunPayload]; + + // Context menu command chain + [Events.PossibleContextMenuCommand]: [interaction: ContextMenuInteraction]; + [Events.UnknownContextMenuCommand]: [payload: UnknownContextMenuCommandPayload]; + [Events.CommandDoesNotHaveContextMenuCommandHandler]: [payload: CommandDoesNotHaveContextMenuCommandHandlerPayload]; + [Events.PreContextMenuCommandRun]: [payload: PreContextMenuCommandRunPayload]; + + [Events.ContextMenuCommandDenied]: [error: UserError, payload: ContextMenuCommandDeniedPayload]; + [Events.ContextMenuCommandAccepted]: [payload: ContextMenuCommandAcceptedPayload]; + + [Events.ContextMenuCommandRun]: [interaction: ContextMenuInteraction, command: ContextMenuCommand, payload: ContextMenuCommandRunPayload]; + [Events.ContextMenuCommandSuccess]: [payload: ContextMenuCommandSuccessPayload]; + [Events.ContextMenuCommandError]: [error: unknown, payload: ContextMenuCommandErrorPayload]; + [Events.ContextMenuCommandFinish]: [interaction: ContextMenuInteraction, command: ContextMenuCommand, payload: ContextMenuCommandRunPayload]; + // #endregion Sapphire load cycle events // #region Termination diff --git a/src/lib/utils/application-commands/ApplicationCommandRegistries.ts b/src/lib/utils/application-commands/ApplicationCommandRegistries.ts new file mode 100644 index 000000000..326f15f5c --- /dev/null +++ b/src/lib/utils/application-commands/ApplicationCommandRegistries.ts @@ -0,0 +1,61 @@ +import { container } from '@sapphire/pieces'; +import { RegisterBehavior } from '../../types/Enums'; +import { ApplicationCommandRegistry } from './ApplicationCommandRegistry'; +import { getNeededRegistryParameters } from './getNeededParameters'; + +export let defaultBehaviorWhenNotIdentical = RegisterBehavior.LogToConsole; + +export const registries = new Map(); + +/** + * Acquires a registry for a command by its name. + * @param commandName The name of the command. + * @returns The application command registry for the command + */ +export function acquire(commandName: string) { + const existing = registries.get(commandName); + if (existing) { + return existing; + } + + const newRegistry = new ApplicationCommandRegistry(commandName); + registries.set(commandName, newRegistry); + + return newRegistry; +} + +/** + * Sets the default behavior when registered commands aren't identical to provided data. + * @param behavior The default behavior to have. Set this to `null` to reset it to the default + * of `RegisterBehavior.LogToConsole`. + */ +export function setDefaultBehaviorWhenNotIdentical(behavior?: RegisterBehavior | null) { + defaultBehaviorWhenNotIdentical = behavior ?? RegisterBehavior.LogToConsole; +} + +export function getDefaultBehaviorWhenNotIdentical() { + return defaultBehaviorWhenNotIdentical; +} + +export async function handleRegistryAPICalls() { + const commandStore = container.stores.get('commands'); + + const { applicationCommands, globalCommands, guildCommands } = await getNeededRegistryParameters(); + + for (const registry of registries.values()) { + // eslint-disable-next-line @typescript-eslint/dot-notation + await registry['runAPICalls'](applicationCommands, globalCommands, guildCommands); + + const piece = registry.command; + + if (piece) { + for (const nameOrId of piece.applicationCommandRegistry.chatInputCommands) { + commandStore.aliases.set(nameOrId, piece); + } + + for (const nameOrId of piece.applicationCommandRegistry.contextMenuCommands) { + commandStore.aliases.set(nameOrId, piece); + } + } + } +} diff --git a/src/lib/utils/application-commands/ApplicationCommandRegistry.ts b/src/lib/utils/application-commands/ApplicationCommandRegistry.ts new file mode 100644 index 000000000..9bba358ad --- /dev/null +++ b/src/lib/utils/application-commands/ApplicationCommandRegistry.ts @@ -0,0 +1,465 @@ +import type { + ContextMenuCommandBuilder, + SlashCommandBuilder, + SlashCommandSubcommandsOnlyBuilder, + SlashCommandOptionsOnlyBuilder +} from '@discordjs/builders'; +import { container } from '@sapphire/pieces'; +import { + ApplicationCommandType, + RESTPostAPIChatInputApplicationCommandsJSONBody, + RESTPostAPIContextMenuApplicationCommandsJSONBody +} from 'discord-api-types/v9'; +import type { + ApplicationCommand, + ApplicationCommandManager, + ChatInputApplicationCommandData, + Collection, + Constants, + MessageApplicationCommandData, + UserApplicationCommandData +} from 'discord.js'; +import { InternalRegistryAPIType, RegisterBehavior } from '../../types/Enums'; +import { getDefaultBehaviorWhenNotIdentical } from './ApplicationCommandRegistries'; +import { CommandDifference, getCommandDifferences } from './computeDifferences'; +import { convertApplicationCommandToApiData, normalizeChatInputCommand, normalizeContextMenuCommand } from './normalizeInputs'; + +export class ApplicationCommandRegistry { + public readonly commandName: string; + + public readonly chatInputCommands = new Set(); + public readonly contextMenuCommands = new Set(); + + private readonly apiCalls: InternalAPICall[] = []; + + public constructor(commandName: string) { + this.commandName = commandName; + } + + public get command() { + return container.stores.get('commands').get(this.commandName); + } + + public registerChatInputCommand( + command: + | ChatInputApplicationCommandData + | SlashCommandBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder + | Omit + | (( + builder: SlashCommandBuilder + ) => + | SlashCommandBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder + | Omit), + options?: ApplicationCommandRegistryRegisterOptions + ) { + const builtData = normalizeChatInputCommand(command); + + this.chatInputCommands.add(builtData.name); + + this.apiCalls.push({ + builtData, + registerOptions: options ?? { registerCommandIfMissing: true, behaviorWhenNotIdentical: getDefaultBehaviorWhenNotIdentical() }, + type: InternalRegistryAPIType.ChatInput + }); + + if (options?.idHints) { + for (const hint of options.idHints) { + this.chatInputCommands.add(hint); + } + } + + return this; + } + + public registerContextMenuCommand( + command: + | UserApplicationCommandData + | MessageApplicationCommandData + | ContextMenuCommandBuilder + | ((builder: ContextMenuCommandBuilder) => ContextMenuCommandBuilder), + options?: ApplicationCommandRegistryRegisterOptions + ) { + const builtData = normalizeContextMenuCommand(command); + + this.contextMenuCommands.add(builtData.name); + + this.apiCalls.push({ + builtData, + registerOptions: options ?? { registerCommandIfMissing: true, behaviorWhenNotIdentical: getDefaultBehaviorWhenNotIdentical() }, + type: InternalRegistryAPIType.ContextMenu + }); + + if (options?.idHints) { + for (const hint of options.idHints) { + this.contextMenuCommands.add(hint); + } + } + + return this; + } + + public addChatInputCommandNames(...names: string[] | string[][]) { + const flattened = names.flat(Infinity) as string[]; + + for (const command of flattened) { + this.debug(`Registering name "${command}" to internal chat input map`); + this.warn( + `Registering the chat input command "${command}" using a name is not recommended.`, + 'Please use the "addChatInputCommandIds" method instead with a command id.' + ); + this.chatInputCommands.add(command); + } + + return this; + } + + public addContextMenuCommandNames(...names: string[] | string[][]) { + const flattened = names.flat(Infinity) as string[]; + + for (const command of flattened) { + this.debug(`Registering name "${command}" to internal context menu map`); + this.warn( + `Registering the context menu command "${command}" using a name is not recommended.`, + 'Please use the "addContextMenuCommandIds" method instead with a command id.' + ); + this.contextMenuCommands.add(command); + } + + return this; + } + + public addChatInputCommandIds(...commandIds: string[] | string[][]) { + const flattened = commandIds.flat(Infinity) as string[]; + + for (const entry of flattened) { + try { + BigInt(entry); + this.debug(`Registering id "${entry}" to internal chat input map`); + } catch { + // Don't be silly, save yourself the headaches and do as we say + this.debug(`Registering name "${entry}" to internal chat input map`); + this.warn( + `Registering the chat input command "${entry}" using a name *and* trying to bypass this warning by calling "addChatInputCommandIds" is not recommended.`, + 'Please use the "addChatInputCommandIds" method with a valid command id instead.' + ); + } + this.chatInputCommands.add(entry); + } + + return this; + } + + public addContextMenuCommandIds(...commandIds: string[] | string[][]) { + const flattened = commandIds.flat(Infinity) as string[]; + + for (const entry of flattened) { + try { + BigInt(entry); + this.debug(`Registering id "${entry}" to internal context menu map`); + } catch { + this.debug(`Registering name "${entry}" to internal context menu map`); + // Don't be silly, save yourself the headaches and do as we say + this.warn( + `Registering the context menu command "${entry}" using a name *and* trying to bypass this warning by calling "addContextMenuCommandIds" is not recommended.`, + 'Please use the "addContextMenuCommandIds" method with a valid command id instead.' + ); + } + this.contextMenuCommands.add(entry); + } + + return this; + } + + protected async runAPICalls( + applicationCommands: ApplicationCommandManager, + globalCommands: Collection, + guildCommands: Map> + ) { + this.debug(`Preparing to process ${this.apiCalls.length} possible command registrations / updates...`); + + const results = await Promise.allSettled( + this.apiCalls.map((call) => this.handleAPICall(applicationCommands, globalCommands, guildCommands, call)) + ); + + const errored = results.filter((result) => result.status === 'rejected') as PromiseRejectedResult[]; + + if (errored.length) { + this.error(`Received ${errored.length} errors while processing command registrations / updates`); + + for (const error of errored) { + this.error(error.reason.stack ?? error.reason); + } + } + } + + private async handleAPICall( + commandsManager: ApplicationCommandManager, + globalCommands: Collection, + allGuildsCommands: Map>, + apiCall: InternalAPICall + ) { + const { builtData, registerOptions } = apiCall; + const commandName = builtData.name; + const behaviorIfNotEqual = registerOptions.behaviorWhenNotIdentical ?? getDefaultBehaviorWhenNotIdentical(); + + const findCallback = (entry: ApplicationCommand) => { + // If the command is a chat input command, we need to check if the entry is a chat input command + if (apiCall.type === InternalRegistryAPIType.ChatInput && entry.type !== 'CHAT_INPUT') return false; + // If the command is a context menu command, we need to check if the entry is a context menu command of the same type + if (apiCall.type === InternalRegistryAPIType.ContextMenu) { + if (entry.type === 'CHAT_INPUT') return false; + + let apiCallType: keyof typeof Constants['ApplicationCommandTypes']; + + switch (apiCall.builtData.type) { + case ApplicationCommandType.Message: + apiCallType = 'MESSAGE'; + break; + case ApplicationCommandType.User: + apiCallType = 'USER'; + break; + default: + throw new Error(`Unhandled context command type: ${apiCall.builtData.type}`); + } + + if (apiCallType !== entry.type) return false; + } + + // Find the command by name or by id hint (mostly useful for context menus) + const isInIdHint = registerOptions.idHints?.includes(entry.id); + return typeof isInIdHint === 'boolean' ? isInIdHint || entry.name === commandName : entry.name === commandName; + }; + + let type: string; + + switch (apiCall.type) { + case InternalRegistryAPIType.ChatInput: + type = 'chat input'; + break; + case InternalRegistryAPIType.ContextMenu: + switch (apiCall.builtData.type) { + case ApplicationCommandType.Message: + type = 'message context menu'; + break; + case ApplicationCommandType.User: + type = 'user context menu'; + break; + default: + type = 'unknown-type context menu'; + } + break; + default: + type = 'unknown'; + } + + if (!registerOptions.guildIds?.length) { + const globalCommand = globalCommands.find(findCallback); + + if (globalCommand) { + switch (apiCall.type) { + case InternalRegistryAPIType.ChatInput: + this.addChatInputCommandIds(globalCommand.id); + break; + case InternalRegistryAPIType.ContextMenu: + this.addContextMenuCommandIds(globalCommand.id); + break; + } + + this.debug(`Checking if command "${commandName}" is identical with global ${type} command with id "${globalCommand.id}"`); + await this.handleCommandPresent(globalCommand, builtData, behaviorIfNotEqual); + } else if (registerOptions.registerCommandIfMissing ?? true) { + this.debug(`Creating new global ${type} command with name "${commandName}"`); + await this.createMissingCommand(commandsManager, builtData, type); + } else { + this.debug(`Doing nothing about missing global ${type} command with name "${commandName}"`); + } + + return; + } + + for (const guildId of registerOptions.guildIds) { + const guildCommands = allGuildsCommands.get(guildId); + + if (!guildCommands) { + this.debug(`There are no commands for guild with id "${guildId}". Will create ${type} command "${commandName}".`); + await this.createMissingCommand(commandsManager, builtData, type, guildId); + continue; + } + + const existingGuildCommand = guildCommands.find(findCallback); + + if (existingGuildCommand) { + this.debug(`Checking if guild ${type} command "${commandName}" is identical to command "${existingGuildCommand.id}"`); + + switch (apiCall.type) { + case InternalRegistryAPIType.ChatInput: + this.addChatInputCommandIds(existingGuildCommand.id); + break; + case InternalRegistryAPIType.ContextMenu: + this.addContextMenuCommandIds(existingGuildCommand.id); + break; + } + + await this.handleCommandPresent(existingGuildCommand, builtData, behaviorIfNotEqual); + } else if (registerOptions.registerCommandIfMissing ?? true) { + this.debug(`Creating new guild ${type} command with name "${commandName}" for guild "${guildId}"`); + await this.createMissingCommand(commandsManager, builtData, type, guildId); + } else { + this.debug(`Doing nothing about missing guild ${type} command with name "${commandName}" for guild "${guildId}"`); + } + } + } + + private async handleCommandPresent( + applicationCommand: ApplicationCommand, + apiData: InternalAPICall['builtData'], + behaviorIfNotEqual: RegisterBehavior, + guildId?: string + ) { + const now = Date.now(); + + // Step 0: compute differences + const differences = getCommandDifferences(convertApplicationCommandToApiData(applicationCommand), apiData); + + const later = Date.now() - now; + this.debug(`Took ${later}ms to process differences`); + + // Step 1: if there are no differences, return + if (!differences.length) { + this.debug( + `${guildId ? 'Guild command' : 'Command'} "${apiData.name}" is identical to command "${applicationCommand.name}" (${ + applicationCommand.id + })` + ); + return; + } + + this.logCommandDifferences(differences, applicationCommand, behaviorIfNotEqual === RegisterBehavior.LogToConsole); + + // Step 2: if the behavior is to log to console, log the differences + if (behaviorIfNotEqual === RegisterBehavior.LogToConsole) { + return; + } + + // Step 3: if the behavior is to update, update the command + try { + await applicationCommand.edit(apiData as ChatInputApplicationCommandData); + this.debug(`Updated command ${applicationCommand.name} (${applicationCommand.id}) with new api data`); + } catch (error) { + this.error(`Failed to update command ${applicationCommand.name} (${applicationCommand.id})`, error); + } + } + + private logCommandDifferences(differences: CommandDifference[], applicationCommand: ApplicationCommand, logAsWarn: boolean) { + const finalMessage: string[] = []; + const pad = ' '.repeat(5); + + for (const difference of differences) { + finalMessage.push( + [ + `└── At path: ${difference.key}`, // + `${pad}├── Received: ${difference.original}`, + `${pad}└── Expected: ${difference.expected}`, + '' + ].join('\n') + ); + } + + const header = `Found differences for command "${applicationCommand.name}" (${applicationCommand.id}) versus provided api data\n`; + + logAsWarn ? this.warn(header, ...finalMessage) : this.debug(header, ...finalMessage); + } + + private async createMissingCommand( + commandsManager: ApplicationCommandManager, + apiData: InternalAPICall['builtData'], + type: string, + guildId?: string + ) { + try { + const result = await commandsManager.create(apiData, guildId); + + this.info( + `Successfully created ${type}${guildId ? ' guild' : ''} command "${apiData.name}" with id "${ + result.id + }". You should add the id to the "idHints" property of the register method you used!` + ); + + switch (apiData.type) { + case undefined: + case ApplicationCommandType.ChatInput: + this.addChatInputCommandIds(result.id); + break; + case ApplicationCommandType.Message: + case ApplicationCommandType.User: + this.addContextMenuCommandIds(result.id); + break; + } + } catch (err) { + this.error( + `Failed to register${guildId ? ' guild' : ''} application command with name "${apiData.name}"${ + guildId ? ` for guild "${guildId}"` : '' + }`, + err + ); + } + } + + private info(message: string, ...other: unknown[]) { + container.logger.info(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other); + } + + private error(message: string, ...other: unknown[]) { + container.logger.error(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other); + } + + private warn(message: string, ...other: unknown[]) { + container.logger.warn(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other); + } + + private debug(message: string, ...other: unknown[]) { + container.logger.debug(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other); + } +} + +export namespace ApplicationCommandRegistry { + export interface RegisterOptions { + /** + * If this is specified, the application commands will only be registered for these guild ids. + */ + guildIds?: string[]; + /** + * If we should register the command when it is missing + * @default true + */ + registerCommandIfMissing?: boolean; + /** + * Specifies what we should do when the command is present, but not identical with the data you provided + * @default `ApplicationCommandRegistries.getDefaultBehaviorWhenNotIdentical` + */ + behaviorWhenNotIdentical?: RegisterBehavior; + /** + * Specifies a list of command ids that we should check in the event of a name mismatch + * @default [] + */ + idHints?: string[]; + } +} + +export type ApplicationCommandRegistryRegisterOptions = ApplicationCommandRegistry.RegisterOptions; + +export type InternalAPICall = + | { + builtData: RESTPostAPIChatInputApplicationCommandsJSONBody; + registerOptions: ApplicationCommandRegistryRegisterOptions; + type: InternalRegistryAPIType.ChatInput; + } + | { + builtData: RESTPostAPIContextMenuApplicationCommandsJSONBody; + registerOptions: ApplicationCommandRegistryRegisterOptions; + type: InternalRegistryAPIType.ContextMenu; + }; diff --git a/src/lib/utils/application-commands/computeDifferences.ts b/src/lib/utils/application-commands/computeDifferences.ts new file mode 100644 index 000000000..aeefd8d68 --- /dev/null +++ b/src/lib/utils/application-commands/computeDifferences.ts @@ -0,0 +1,434 @@ +import { + APIApplicationCommandIntegerOption, + APIApplicationCommandNumberOption, + APIApplicationCommandOption, + APIApplicationCommandOptionChoice, + APIApplicationCommandStringOption, + APIApplicationCommandSubcommandGroupOption, + APIApplicationCommandSubcommandOption, + ApplicationCommandOptionType, + ApplicationCommandType, + RESTPostAPIApplicationCommandsJSONBody, + RESTPostAPIChatInputApplicationCommandsJSONBody, + RESTPostAPIContextMenuApplicationCommandsJSONBody +} from 'discord-api-types/v9'; +import type { InternalAPICall } from './ApplicationCommandRegistry'; + +const optionTypeToPrettyName = new Map([ + [ApplicationCommandOptionType.Subcommand, 'subcommand'], + [ApplicationCommandOptionType.SubcommandGroup, 'subcommand group'], + [ApplicationCommandOptionType.String, 'string option'], + [ApplicationCommandOptionType.Integer, 'integer option'], + [ApplicationCommandOptionType.Boolean, 'boolean option'], + [ApplicationCommandOptionType.User, 'user option'], + [ApplicationCommandOptionType.Channel, 'channel option'], + [ApplicationCommandOptionType.Role, 'role option'], + [ApplicationCommandOptionType.Mentionable, 'mentionable option'], + [ApplicationCommandOptionType.Number, 'number option'] +]); + +const contextMenuTypes = [ApplicationCommandType.Message, ApplicationCommandType.User]; +const subcommandTypes = [ApplicationCommandOptionType.SubcommandGroup, ApplicationCommandOptionType.Subcommand]; + +type APIApplicationCommandSubcommandTypes = APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption; +type APIApplicationCommandNumericTypes = APIApplicationCommandIntegerOption | APIApplicationCommandNumberOption; +type APIApplicationCommandChoosableAndAutocompletableTypes = APIApplicationCommandNumericTypes | APIApplicationCommandStringOption; + +export function getCommandDifferences(existingCommand: RESTPostAPIApplicationCommandsJSONBody, apiData: InternalAPICall['builtData']) { + const differences: CommandDifference[] = []; + + if (existingCommand.type !== ApplicationCommandType.ChatInput && existingCommand.type) { + // Check context menus + if (contextMenuTypes.includes(existingCommand.type ?? ApplicationCommandType.ChatInput)) { + const casted = apiData as RESTPostAPIContextMenuApplicationCommandsJSONBody; + + // Check name + if (existingCommand.name !== casted.name) { + differences.push({ + key: 'name', + original: existingCommand.name, + expected: casted.name + }); + } + + // Check defaultPermissions + // TODO(vladfrangu): This will be deprecated + if ((existingCommand.default_permission ?? true) !== (casted.default_permission ?? true)) { + differences.push({ + key: 'defaultPermission', + original: String(existingCommand.default_permission ?? true), + expected: String(casted.default_permission ?? true) + }); + } + } + + return differences; + } + + const casted = apiData as RESTPostAPIChatInputApplicationCommandsJSONBody; + + // Check name + if (existingCommand.name.toLowerCase() !== casted.name.toLowerCase()) { + differences.push({ + key: 'name', + original: existingCommand.name, + expected: casted.name + }); + } + + // Check defaultPermissions + // TODO(vladfrangu): This will be deprecated + if ((existingCommand.default_permission ?? true) !== (casted.default_permission ?? true)) { + differences.push({ + key: 'defaultPermission', + original: String(existingCommand.default_permission ?? true), + expected: String(casted.default_permission ?? true) + }); + } + + // Check description + if (existingCommand.description !== casted.description) { + differences.push({ + key: 'description', + original: existingCommand.description, + expected: casted.description + }); + } + + // 0. No existing options and now we have options + if (!existingCommand.options?.length && casted.options?.length) { + differences.push({ + key: 'options', + original: 'no options present', + expected: 'options present' + }); + } + // 1. Existing options and now we have no options + else if (existingCommand.options?.length && !casted.options?.length) { + differences.push({ + key: 'options', + original: 'options present', + expected: 'no options present' + }); + } + // 2. Iterate over each option if we have any and see what's different + else if (casted.options?.length) { + let index = 0; + for (const option of casted.options) { + const currentIndex = index++; + const existingOption = existingCommand.options![currentIndex]; + differences.push(...reportOptionDifferences({ currentIndex, option, existingOption })); + } + + // If we went through less options than we previously had, report that + if (index < existingCommand.options!.length) { + let option: APIApplicationCommandOption; + while ((option = existingCommand.options![index]) !== undefined) { + const expectedType = + optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; + + differences.push({ + key: `existing command option at index ${index}`, + expected: 'no option present', + original: `${expectedType} with name ${option.name}` + }); + + index++; + } + } + } + + return differences; +} + +export interface CommandDifference { + key: string; + expected: string; + original: string; +} + +function* reportOptionDifferences({ + option, + existingOption, + currentIndex, + keyPath = (index: number) => `options[${index}]` +}: { + option: APIApplicationCommandOption; + currentIndex: number; + existingOption?: APIApplicationCommandOption; + keyPath?: (index: number) => string; +}): Generator { + const expectedType = optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; + + // If current option doesn't exist, report and return + if (!existingOption) { + yield { + key: keyPath(currentIndex), + expected: `${expectedType} with name ${option.name}`, + original: 'no option present' + }; + return; + } + + // Check type + if (existingOption.type !== option.type) { + yield { + key: `${keyPath(currentIndex)}.type`, + original: + optionTypeToPrettyName.get(existingOption.type) ?? `unknown (${existingOption.type}); please contact Sapphire developers about this!`, + expected: expectedType + }; + } + + // Check name + if (existingOption.name !== option.name) { + yield { + key: `${keyPath(currentIndex)}.name`, + original: existingOption.name, + expected: option.name + }; + } + + // Check description + if (existingOption.description !== option.description) { + yield { + key: `${keyPath(currentIndex)}.description`, + original: existingOption.description, + expected: option.description + }; + } + + // Check required + if ((existingOption.required ?? false) !== (option.required ?? false)) { + yield { + key: `${keyPath(currentIndex)}.required`, + original: String(existingOption.required ?? false), + expected: String(option.required ?? false) + }; + } + + // Check for subcommands + if (subcommandTypes.includes(existingOption.type) && subcommandTypes.includes(option.type)) { + const castedExisting = existingOption as APIApplicationCommandSubcommandTypes; + const castedExpected = option as APIApplicationCommandSubcommandTypes; + + if ( + castedExisting.type === ApplicationCommandOptionType.SubcommandGroup && + castedExpected.type === ApplicationCommandOptionType.SubcommandGroup + ) { + // We know we have options in this case, because they are both groups + for (const [subcommandIndex, subcommandOption] of castedExpected.options!.entries()) { + yield* reportOptionDifferences({ + currentIndex: subcommandIndex, + option: subcommandOption, + existingOption: castedExisting.options?.[subcommandIndex], + keyPath: (index) => `${keyPath(currentIndex)}.options[${index}]` + }); + } + } else if ( + castedExisting.type === ApplicationCommandOptionType.Subcommand && + castedExpected.type === ApplicationCommandOptionType.Subcommand + ) { + // 0. No existing options and now we have options + if (!castedExisting.options?.length && castedExpected.options?.length) { + yield { + key: `${keyPath(currentIndex)}.options`, + expected: 'options present', + original: 'no options present' + }; + } + // 1. Existing options and now we have no options + else if (castedExisting.options?.length && !castedExpected.options?.length) { + yield { + key: `${keyPath(currentIndex)}.options`, + expected: 'no options present', + original: 'options present' + }; + } + // 2. Iterate over each option if we have any and see what's different + else if (castedExpected.options?.length) { + let processedIndex = 0; + for (const subcommandOption of castedExpected.options) { + const currentSubCommandOptionIndex = processedIndex++; + const existingSubcommandOption = castedExisting.options![currentSubCommandOptionIndex]; + + yield* reportOptionDifferences({ + currentIndex: currentSubCommandOptionIndex, + option: subcommandOption, + existingOption: existingSubcommandOption, + keyPath: (index) => `${keyPath(currentIndex)}.options[${index}]` + }); + } + + // If we went through less options than we previously had, report that + if (processedIndex < castedExisting.options!.length) { + let option: APIApplicationCommandOption; + while ((option = castedExisting.options![processedIndex]) !== undefined) { + const expectedType = + optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; + + yield { + key: `existing command option at path ${keyPath(currentIndex)}.options[${processedIndex}]`, + expected: 'no option present', + original: `${expectedType} with name ${option.name}` + }; + + processedIndex++; + } + } + } + } + } + + if (hasMinMaxValueSupport(option)) { + // Check min and max_value + const existingCasted = existingOption as APIApplicationCommandNumericTypes; + + // 0. No min_value and now we have min_value + if (existingCasted.min_value === undefined && option.min_value !== undefined) { + yield { + key: `${keyPath(currentIndex)}.min_value`, + expected: 'min_value present', + original: 'no min_value present' + }; + } + // 1. Have min_value and now we don't + else if (existingCasted.min_value !== undefined && option.min_value === undefined) { + yield { + key: `${keyPath(currentIndex)}.min_value`, + expected: 'no min_value present', + original: 'min_value present' + }; + } + // 2. Equality check + else if (existingCasted.min_value !== option.min_value) { + yield { + key: `${keyPath(currentIndex)}.min_value`, + original: String(existingCasted.min_value), + expected: String(option.min_value) + }; + } + + // 0. No max_value and now we have max_value + if (existingCasted.max_value === undefined && option.max_value !== undefined) { + yield { + key: `${keyPath(currentIndex)}.max_value`, + expected: 'max_value present', + original: 'no max_value present' + }; + } + // 1. Have max_value and now we don't + else if (existingCasted.max_value !== undefined && option.max_value === undefined) { + yield { + key: `${keyPath(currentIndex)}.max_value`, + expected: 'no max_value present', + original: 'max_value present' + }; + } + // 2. Equality check + else if (existingCasted.max_value !== option.max_value) { + yield { + key: `${keyPath(currentIndex)}.max_value`, + original: String(existingCasted.max_value), + expected: String(option.max_value) + }; + } + } + + if (hasChoicesAndAutocompleteSupport(option)) { + const existingCasted = existingOption as APIApplicationCommandChoosableAndAutocompletableTypes; + + // 0. No autocomplete and now it should autocomplete + if (!existingCasted.autocomplete && option.autocomplete) { + yield { + key: `${keyPath(currentIndex)}.autocomplete`, + expected: 'autocomplete enabled', + original: 'autocomplete disabled' + }; + } + // 1. Have autocomplete and now it shouldn't + else if (existingCasted.autocomplete && !option.autocomplete) { + yield { + key: `${keyPath(currentIndex)}.autocomplete`, + expected: 'autocomplete disabled', + original: 'autocomplete enabled' + }; + } + + if (!option.autocomplete && !existingCasted.autocomplete) { + // 0. No choices and now we have choices + if (!existingCasted.choices?.length && option.choices?.length) { + yield { + key: `${keyPath(currentIndex)}.choices`, + expected: 'choices present', + original: 'no choices present' + }; + } + // 1. Have choices and now we don't + else if (existingCasted.choices?.length && !option.choices?.length) { + yield { + key: `${keyPath(currentIndex)}.choices`, + expected: 'no choices present', + original: 'choices present' + }; + } + // 2. Check every choice to see differences + else if (option.choices?.length && existingCasted.choices?.length) { + let index = 0; + for (const choice of option.choices) { + const currentChoiceIndex = index++; + const existingChoice = existingCasted.choices[currentChoiceIndex]; + + // If this choice never existed, return the difference + if (existingChoice === undefined) { + yield { + key: `${keyPath(currentIndex)}.choices[${currentChoiceIndex}]`, + expected: 'no choice present', + original: 'choice present' + }; + } else { + if (choice.name !== existingChoice.name) { + yield { + key: `${keyPath(currentIndex)}.choices[${currentChoiceIndex}].name`, + original: existingChoice.name, + expected: choice.name + }; + } + + if (choice.value !== existingChoice.value) { + yield { + key: `${keyPath(currentIndex)}.choices[${currentChoiceIndex}].value`, + original: String(existingChoice.value), + expected: String(choice.value) + }; + } + } + } + + // If there are more choices than the expected ones, return the difference + if (index < existingCasted.choices.length) { + let choice: APIApplicationCommandOptionChoice; + while ((choice = existingCasted.choices[index]) !== undefined) { + yield { + key: `existing choice at path ${keyPath(currentIndex)}.choices[${index}]`, + expected: 'no choice present', + original: `choice with name "${choice.name}" and value ${ + typeof choice.value === 'number' ? choice.value : `"${choice.value}"` + } present` + }; + + index++; + } + } + } + } + } +} + +function hasMinMaxValueSupport(option: APIApplicationCommandOption): option is APIApplicationCommandNumericTypes { + return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number].includes(option.type); +} + +function hasChoicesAndAutocompleteSupport(option: APIApplicationCommandOption): option is APIApplicationCommandChoosableAndAutocompletableTypes { + return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number, ApplicationCommandOptionType.String].includes(option.type); +} diff --git a/src/lib/utils/application-commands/emitRegistryError.ts b/src/lib/utils/application-commands/emitRegistryError.ts new file mode 100644 index 000000000..3622e5b26 --- /dev/null +++ b/src/lib/utils/application-commands/emitRegistryError.ts @@ -0,0 +1,22 @@ +import { container } from '@sapphire/pieces'; +import type { Command } from '../../structures/Command'; +import { Events } from '../../types/Events'; + +/** + * Opinionatedly logs the encountered registry error. + * @param error The emitted error + * @param command The command which had the error + */ +export function emitRegistryError(error: unknown, command: Command) { + const { name, location } = command; + const { client, logger } = container; + + if (client.listenerCount(Events.CommandApplicationCommandRegistryError)) { + client.emit(Events.CommandApplicationCommandRegistryError, error, command); + } else { + logger.error( + `Encountered error while handling the command application command registry for command "${name}" at path "${location.full}"`, + error + ); + } +} diff --git a/src/lib/utils/application-commands/getNeededParameters.ts b/src/lib/utils/application-commands/getNeededParameters.ts new file mode 100644 index 000000000..11ddcaab6 --- /dev/null +++ b/src/lib/utils/application-commands/getNeededParameters.ts @@ -0,0 +1,34 @@ +import { container } from '@sapphire/pieces'; +import type { ApplicationCommand, ApplicationCommandManager, Collection } from 'discord.js'; + +export async function getNeededRegistryParameters() { + const { client } = container; + + const applicationCommands = client.application!.commands; + const globalCommands = await applicationCommands.fetch(); + const guildCommands = await fetchGuildCommands(applicationCommands); + + return { + applicationCommands, + globalCommands, + guildCommands + }; +} + +async function fetchGuildCommands(commands: ApplicationCommandManager) { + const map = new Map>(); + + for (const [guildId, guild] of commands.client.guilds.cache.entries()) { + try { + const guildCommands = await commands.fetch({ guildId }); + map.set(guildId, guildCommands); + } catch (err) { + container.logger.warn( + `ApplicationCommandRegistries: Failed to fetch guild commands for guild "${guild.name}" (${guildId}).`, + 'Make sure to authorize your application with the "applications.commands" scope in that guild.' + ); + } + } + + return map; +} diff --git a/src/lib/utils/application-commands/normalizeInputs.ts b/src/lib/utils/application-commands/normalizeInputs.ts new file mode 100644 index 000000000..3c66b916b --- /dev/null +++ b/src/lib/utils/application-commands/normalizeInputs.ts @@ -0,0 +1,134 @@ +import { + ContextMenuCommandBuilder, + SlashCommandBuilder, + SlashCommandSubcommandsOnlyBuilder, + SlashCommandOptionsOnlyBuilder +} from '@discordjs/builders'; +import { isFunction } from '@sapphire/utilities'; +import { + ApplicationCommandType, + APIApplicationCommandOption, + RESTPostAPIApplicationCommandsJSONBody, + RESTPostAPIChatInputApplicationCommandsJSONBody, + RESTPostAPIContextMenuApplicationCommandsJSONBody +} from 'discord-api-types/v9'; +import { + ApplicationCommand, + ChatInputApplicationCommandData, + Constants, + MessageApplicationCommandData, + UserApplicationCommandData +} from 'discord.js'; + +function isBuilder( + command: unknown +): command is + | SlashCommandBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder + | Omit { + return command instanceof SlashCommandBuilder; +} + +export function normalizeChatInputCommand( + command: + | ChatInputApplicationCommandData + | SlashCommandBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder + | Omit + | (( + builder: SlashCommandBuilder + ) => + | SlashCommandBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder + | Omit) +): RESTPostAPIChatInputApplicationCommandsJSONBody { + if (isFunction(command)) { + const builder = new SlashCommandBuilder(); + command(builder); + return builder.toJSON() as RESTPostAPIChatInputApplicationCommandsJSONBody; + } + + if (isBuilder(command)) { + return command.toJSON() as RESTPostAPIChatInputApplicationCommandsJSONBody; + } + + const finalObject = { + description: command.description, + name: command.name, + default_permission: command.defaultPermission, + type: ApplicationCommandType.ChatInput + } as RESTPostAPIChatInputApplicationCommandsJSONBody; + + if (command.options?.length) { + finalObject.options = command.options.map((option) => ApplicationCommand['transformOption'](option) as APIApplicationCommandOption); + } + + return finalObject; +} + +export function normalizeContextMenuCommand( + command: + | UserApplicationCommandData + | MessageApplicationCommandData + | ContextMenuCommandBuilder + | ((builder: ContextMenuCommandBuilder) => ContextMenuCommandBuilder) +): RESTPostAPIContextMenuApplicationCommandsJSONBody { + if (isFunction(command)) { + const builder = new ContextMenuCommandBuilder(); + command(builder); + return builder.toJSON() as RESTPostAPIContextMenuApplicationCommandsJSONBody; + } + + if (command instanceof ContextMenuCommandBuilder) { + return command.toJSON() as RESTPostAPIContextMenuApplicationCommandsJSONBody; + } + + let type: ApplicationCommandType; + + switch (command.type) { + case Constants.ApplicationCommandTypes.MESSAGE: + case 'MESSAGE': + type = ApplicationCommandType.Message; + break; + case Constants.ApplicationCommandTypes.USER: + case 'USER': + type = ApplicationCommandType.User; + break; + default: + // @ts-expect-error command gets turned to never, which is half true. + throw new Error(`Unhandled command type: ${command.type}`); + } + + const finalObject = { + name: command.name, + type, + default_permission: command.defaultPermission + } as RESTPostAPIContextMenuApplicationCommandsJSONBody; + + return finalObject; +} + +export function convertApplicationCommandToApiData(command: ApplicationCommand): RESTPostAPIApplicationCommandsJSONBody { + const returnData = { + name: command.name, + default_permission: command.defaultPermission + } as RESTPostAPIApplicationCommandsJSONBody; + + if (command.type === 'CHAT_INPUT') { + returnData.type = ApplicationCommandType.ChatInput; + (returnData as RESTPostAPIChatInputApplicationCommandsJSONBody).description = command.description; + } else if (command.type === 'MESSAGE') { + returnData.type = ApplicationCommandType.Message; + } else if (command.type === 'USER') { + returnData.type = ApplicationCommandType.User; + } + + if (command.options.length) { + returnData.options = command.options.map((option) => ApplicationCommand['transformOption'](option as any) as APIApplicationCommandOption); + } + + return returnData; +} diff --git a/src/lib/utils/logger/Logger.ts b/src/lib/utils/logger/Logger.ts index 45f2afce5..60d1bd75d 100644 --- a/src/lib/utils/logger/Logger.ts +++ b/src/lib/utils/logger/Logger.ts @@ -38,7 +38,7 @@ export class Logger implements ILogger { public write(level: LogLevel, ...values: readonly unknown[]): void { if (!this.has(level)) return; const method = Logger.levels.get(level); - if (typeof method === 'string') console[method](...values); + if (typeof method === 'string') console[method](`[${method.toUpperCase()}]`, ...values); } protected static readonly levels = new Map([ diff --git a/src/lib/utils/preconditions/IPreconditionContainer.ts b/src/lib/utils/preconditions/IPreconditionContainer.ts index 6657d1dc4..84d1c2f48 100644 --- a/src/lib/utils/preconditions/IPreconditionContainer.ts +++ b/src/lib/utils/preconditions/IPreconditionContainer.ts @@ -1,5 +1,5 @@ import type { Awaitable } from '@sapphire/utilities'; -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import type { UserError } from '../../errors/UserError'; import type { Result } from '../../parsers/Result'; import type { Command } from '../../structures/Command'; @@ -12,7 +12,7 @@ import type { PreconditionContext } from '../../structures/Precondition'; export type PreconditionContainerResult = Result; /** - * Defines the return type of the generic {@link IPreconditionContainer.run}. + * Defines the return type of the generic {@link IPreconditionContainer.messageRun}. * @since 1.0.0 */ export type PreconditionContainerReturn = Awaitable; @@ -34,5 +34,19 @@ export interface IPreconditionContainer { * @param message The message that ran this precondition. * @param command The command the message invoked. */ - run(message: Message, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + messageRun(message: Message, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + /** + * Runs a precondition container. + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + */ + chatInputRun(interaction: CommandInteraction, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + /** + * Runs a precondition container. + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + */ + contextMenuRun(interaction: ContextMenuInteraction, command: Command, context?: PreconditionContext): PreconditionContainerReturn; } diff --git a/src/lib/utils/preconditions/PreconditionContainerArray.ts b/src/lib/utils/preconditions/PreconditionContainerArray.ts index 12b10b464..0903f6dd8 100644 --- a/src/lib/utils/preconditions/PreconditionContainerArray.ts +++ b/src/lib/utils/preconditions/PreconditionContainerArray.ts @@ -1,5 +1,5 @@ -import { Collection, Message } from 'discord.js'; -import type { Command } from '../../structures/Command'; +import { Collection, CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; +import type { ChatInputCommand, ContextMenuCommand, MessageCommand } from '../../structures/Command'; import type { PreconditionContext, PreconditionKeys, SimplePreconditionKeys } from '../../structures/Precondition'; import type { IPreconditionCondition } from './conditions/IPreconditionCondition'; import { PreconditionConditionAnd } from './conditions/PreconditionConditionAnd'; @@ -164,10 +164,38 @@ export class PreconditionContainerArray implements IPreconditionContainer { * @param message The message that ran this precondition. * @param command The command the message invoked. */ - public run(message: Message, command: Command, context: PreconditionContext = {}): PreconditionContainerReturn { + public messageRun(message: Message, command: MessageCommand, context: PreconditionContext = {}): PreconditionContainerReturn { return this.mode === PreconditionRunMode.Sequential - ? this.condition.sequential(message, command, this.entries, context) - : this.condition.parallel(message, command, this.entries, context); + ? this.condition.messageSequential(message, command, this.entries, context) + : this.condition.messageParallel(message, command, this.entries, context); + } + + /** + * Runs the container. + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + */ + public chatInputRun(interaction: CommandInteraction, command: ChatInputCommand, context: PreconditionContext = {}): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.chatInputSequential(interaction, command, this.entries, context) + : this.condition.chatInputParallel(interaction, command, this.entries, context); + } + + /** + * Runs the container. + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + */ + public contextMenuRun( + interaction: ContextMenuInteraction, + command: ContextMenuCommand, + context: PreconditionContext = {} + ): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.contextMenuSequential(interaction, command, this.entries, context) + : this.condition.contextMenuParallel(interaction, command, this.entries, context); } /** diff --git a/src/lib/utils/preconditions/PreconditionContainerSingle.ts b/src/lib/utils/preconditions/PreconditionContainerSingle.ts index 6131244e9..43dc5ab39 100644 --- a/src/lib/utils/preconditions/PreconditionContainerSingle.ts +++ b/src/lib/utils/preconditions/PreconditionContainerSingle.ts @@ -1,6 +1,6 @@ import { container } from '@sapphire/pieces'; -import type { Message } from 'discord.js'; -import type { Command } from '../../structures/Command'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; +import type { ChatInputCommand, ContextMenuCommand, MessageCommand } from '../../structures/Command'; import type { PreconditionContext, PreconditionKeys, Preconditions, SimplePreconditionKeys } from '../../structures/Precondition'; import type { IPreconditionContainer } from './IPreconditionContainer'; @@ -77,9 +77,48 @@ export class PreconditionContainerSingle implements IPreconditionContainer { * @param message The message that ran this precondition. * @param command The command the message invoked. */ - public run(message: Message, command: Command, context: PreconditionContext = {}) { + public messageRun(message: Message, command: MessageCommand, context: PreconditionContext = {}) { const precondition = container.stores.get('preconditions').get(this.name); - if (precondition) return precondition.run(message, command, { ...context, ...this.context }); + if (precondition) { + if (precondition.messageRun) return precondition.messageRun(message, command, { ...context, ...this.context }); + throw new Error( + `The precondition "${precondition.name}" is missing a "messageRun" handler, but it was requested for the "${command.name}" command.` + ); + } + throw new Error(`The precondition "${this.name}" is not available.`); + } + + /** + * Runs the container. + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + */ + public chatInputRun(interaction: CommandInteraction, command: ChatInputCommand, context: PreconditionContext = {}) { + const precondition = container.stores.get('preconditions').get(this.name); + if (precondition) { + if (precondition.chatInputRun) return precondition.chatInputRun(interaction, command, { ...context, ...this.context }); + throw new Error( + `The precondition "${precondition.name}" is missing a "chatInputRun" handler, but it was requested for the "${command.name}" command.` + ); + } + throw new Error(`The precondition "${this.name}" is not available.`); + } + + /** + * Runs the container. + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + */ + public contextMenuRun(interaction: ContextMenuInteraction, command: ContextMenuCommand, context: PreconditionContext = {}) { + const precondition = container.stores.get('preconditions').get(this.name); + if (precondition) { + if (precondition.contextMenuRun) return precondition.contextMenuRun(interaction, command, { ...context, ...this.context }); + throw new Error( + `The precondition "${precondition.name}" is missing a "contextMenuRun" handler, but it was requested for the "${command.name}" command.` + ); + } throw new Error(`The precondition "${this.name}" is not available.`); } } diff --git a/src/lib/utils/preconditions/conditions/IPreconditionCondition.ts b/src/lib/utils/preconditions/conditions/IPreconditionCondition.ts index ac4ecd29b..327818a45 100644 --- a/src/lib/utils/preconditions/conditions/IPreconditionCondition.ts +++ b/src/lib/utils/preconditions/conditions/IPreconditionCondition.ts @@ -1,5 +1,5 @@ -import type { Message } from 'discord.js'; -import type { Command } from '../../../structures/Command'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; +import type { ChatInputCommand, ContextMenuCommand, MessageCommand } from '../../../structures/Command'; import type { PreconditionContext } from '../../../structures/Precondition'; import type { IPreconditionContainer, PreconditionContainerReturn } from '../IPreconditionContainer'; @@ -16,9 +16,9 @@ export interface IPreconditionCondition { * @param command The command the message invoked. * @param entries The containers to run. */ - sequential( + messageSequential( message: Message, - command: Command, + command: MessageCommand, entries: readonly IPreconditionContainer[], context: PreconditionContext ): PreconditionContainerReturn; @@ -31,9 +31,69 @@ export interface IPreconditionCondition { * @param command The command the message invoked. * @param entries The containers to run. */ - parallel( + messageParallel( message: Message, - command: Command, + command: MessageCommand, + entries: readonly IPreconditionContainer[], + context: PreconditionContext + ): PreconditionContainerReturn; + + /** + * Runs the containers one by one. + * @seealso {@link PreconditionRunMode.sequential} + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + * @param entries The containers to run. + */ + chatInputSequential( + interaction: CommandInteraction, + command: ChatInputCommand, + entries: readonly IPreconditionContainer[], + context: PreconditionContext + ): PreconditionContainerReturn; + + /** + * Runs all the containers using `Promise.all`, then checks the results once all tasks finished running. + * @seealso {@link PreconditionRunMode.parallel} + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + * @param entries The containers to run. + */ + chatInputParallel( + interaction: CommandInteraction, + command: ChatInputCommand, + entries: readonly IPreconditionContainer[], + context: PreconditionContext + ): PreconditionContainerReturn; + + /** + * Runs the containers one by one. + * @seealso {@link PreconditionRunMode.sequential} + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + * @param entries The containers to run. + */ + contextMenuSequential( + interaction: ContextMenuInteraction, + command: ContextMenuCommand, + entries: readonly IPreconditionContainer[], + context: PreconditionContext + ): PreconditionContainerReturn; + + /** + * Runs all the containers using `Promise.all`, then checks the results once all tasks finished running. + * @seealso {@link PreconditionRunMode.parallel} + * @since 3.0.0 + * @param interaction The interaction that ran this precondition. + * @param command The command the interaction invoked. + * @param entries The containers to run. + */ + contextMenuParallel( + interaction: ContextMenuInteraction, + command: ContextMenuCommand, entries: readonly IPreconditionContainer[], context: PreconditionContext ): PreconditionContainerReturn; diff --git a/src/lib/utils/preconditions/conditions/PreconditionConditionAnd.ts b/src/lib/utils/preconditions/conditions/PreconditionConditionAnd.ts index 227df9742..fb78186fd 100644 --- a/src/lib/utils/preconditions/conditions/PreconditionConditionAnd.ts +++ b/src/lib/utils/preconditions/conditions/PreconditionConditionAnd.ts @@ -6,17 +6,45 @@ import type { IPreconditionCondition } from './IPreconditionCondition'; * @since 1.0.0 */ export const PreconditionConditionAnd: IPreconditionCondition = { - async sequential(message, command, entries, context) { + async messageSequential(message, command, entries, context) { for (const child of entries) { - const result = await child.run(message, command, context); + const result = await child.messageRun(message, command, context); if (isErr(result)) return result; } return ok(); }, - async parallel(message, command, entries, context) { - const results = await Promise.all(entries.map((entry) => entry.run(message, command, context))); - // This is simplified compared to PreconditionContainerAny because we're looking for the first error. + async messageParallel(message, command, entries, context) { + const results = await Promise.all(entries.map((entry) => entry.messageRun(message, command, context))); + // This is simplified compared to PreconditionContainerAny, because we're looking for the first error. + // However, the base implementation short-circuits with the first Ok. + return results.find(isErr) ?? ok(); + }, + async chatInputSequential(interaction, command, entries, context) { + for (const child of entries) { + const result = await child.chatInputRun(interaction, command, context); + if (isErr(result)) return result; + } + + return ok(); + }, + async chatInputParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map((entry) => entry.chatInputRun(interaction, command, context))); + // This is simplified compared to PreconditionContainerAny, because we're looking for the first error. + // However, the base implementation short-circuits with the first Ok. + return results.find(isErr) ?? ok(); + }, + async contextMenuSequential(interaction, command, entries, context) { + for (const child of entries) { + const result = await child.contextMenuRun(interaction, command, context); + if (isErr(result)) return result; + } + + return ok(); + }, + async contextMenuParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map((entry) => entry.contextMenuRun(interaction, command, context))); + // This is simplified compared to PreconditionContainerAny, because we're looking for the first error. // However, the base implementation short-circuits with the first Ok. return results.find(isErr) ?? ok(); } diff --git a/src/lib/utils/preconditions/conditions/PreconditionConditionOr.ts b/src/lib/utils/preconditions/conditions/PreconditionConditionOr.ts index e807a5b9e..2cbc02fac 100644 --- a/src/lib/utils/preconditions/conditions/PreconditionConditionOr.ts +++ b/src/lib/utils/preconditions/conditions/PreconditionConditionOr.ts @@ -7,18 +7,60 @@ import type { IPreconditionCondition } from './IPreconditionCondition'; * @since 1.0.0 */ export const PreconditionConditionOr: IPreconditionCondition = { - async sequential(message, command, entries, context) { + async messageSequential(message, command, entries, context) { let error: PreconditionContainerResult | null = null; for (const child of entries) { - const result = await child.run(message, command, context); + const result = await child.messageRun(message, command, context); if (isOk(result)) return result; error = result; } return error ?? ok(); }, - async parallel(message, command, entries, context) { - const results = await Promise.all(entries.map((entry) => entry.run(message, command, context))); + async messageParallel(message, command, entries, context) { + const results = await Promise.all(entries.map((entry) => entry.messageRun(message, command, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (isOk(result)) return result; + error = result; + } + + return error ?? ok(); + }, + async chatInputSequential(interaction, command, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.chatInputRun(interaction, command, context); + if (isOk(result)) return result; + error = result; + } + + return error ?? ok(); + }, + async chatInputParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map((entry) => entry.chatInputRun(interaction, command, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (isOk(result)) return result; + error = result; + } + + return error ?? ok(); + }, + async contextMenuSequential(interaction, command, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.contextMenuRun(interaction, command, context); + if (isOk(result)) return result; + error = result; + } + + return error ?? ok(); + }, + async contextMenuParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map((entry) => entry.contextMenuRun(interaction, command, context))); let error: PreconditionContainerResult | null = null; for (const result of results) { diff --git a/src/listeners/CoreInteractionCreate.ts b/src/listeners/CoreInteractionCreate.ts new file mode 100644 index 000000000..379d30138 --- /dev/null +++ b/src/listeners/CoreInteractionCreate.ts @@ -0,0 +1,24 @@ +import type { PieceContext } from '@sapphire/pieces'; +import type { Interaction } from 'discord.js'; +import { Listener } from '../lib/structures/Listener'; +import { Events } from '../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.InteractionCreate }); + } + + public async run(interaction: Interaction) { + if (interaction.isCommand()) { + this.container.client.emit(Events.PossibleChatInputCommand, interaction); + } else if (interaction.isContextMenu()) { + this.container.client.emit(Events.PossibleContextMenuCommand, interaction); + } else if (interaction.isAutocomplete()) { + this.container.client.emit(Events.PossibleAutocompleteInteraction, interaction); + } else if (interaction.isMessageComponent()) { + await this.container.stores.get('interaction-handlers').run(interaction); + } else { + this.container.logger.warn(`Unhandled interaction type: ${interaction.constructor.name}`); + } + } +} diff --git a/src/listeners/CoreReady.ts b/src/listeners/CoreReady.ts index 42db590ba..8457f8dbc 100644 --- a/src/listeners/CoreReady.ts +++ b/src/listeners/CoreReady.ts @@ -1,13 +1,22 @@ import type { PieceContext } from '@sapphire/pieces'; import { Listener } from '../lib/structures/Listener'; import { Events } from '../lib/types/Events'; +import { handleRegistryAPICalls } from '../lib/utils/application-commands/ApplicationCommandRegistries'; -export class CoreEvent extends Listener { +export class CoreEvent extends Listener { public constructor(context: PieceContext) { super(context, { event: Events.ClientReady, once: true }); } - public run() { + public async run() { this.container.client.id ??= this.container.client.user?.id ?? null; + + this.container.logger.info(`ApplicationCommandRegistries: Initializing...`); + + const now = Date.now(); + await handleRegistryAPICalls(); + const diff = Date.now() - now; + + this.container.logger.info(`ApplicationCommandRegistries: Took ${diff.toLocaleString()}ms to initialize.`); } } diff --git a/src/listeners/application-commands/CorePossibleAutocompleteInteraction.ts b/src/listeners/application-commands/CorePossibleAutocompleteInteraction.ts new file mode 100644 index 000000000..1fc6fb6f5 --- /dev/null +++ b/src/listeners/application-commands/CorePossibleAutocompleteInteraction.ts @@ -0,0 +1,41 @@ +import type { PieceContext } from '@sapphire/pieces'; +import type { AutocompleteInteraction } from 'discord.js'; +import type { AutocompleteCommand } from '../../lib/structures/Command'; +import { Listener } from '../../lib/structures/Listener'; +import { Events } from '../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.PossibleAutocompleteInteraction }); + } + + public async run(interaction: AutocompleteInteraction) { + const { stores } = this.container; + + const commandStore = stores.get('commands'); + + // Try resolving in command + const command = commandStore.get(interaction.commandId) ?? commandStore.get(interaction.commandName); + + if (command?.autocompleteRun) { + try { + await command.autocompleteRun(interaction); + this.container.client.emit(Events.CommandAutocompleteInteractionSuccess, { + command: command as AutocompleteCommand, + context: { commandId: interaction.commandId, commandName: interaction.commandName }, + interaction + }); + } catch (err) { + this.container.client.emit(Events.CommandAutocompleteInteractionError, err, { + command: command as AutocompleteCommand, + context: { commandId: interaction.commandId, commandName: interaction.commandName }, + interaction + }); + } + return; + } + + // Unless we ran a command handler, always call interaction handlers with the interaction + await this.container.stores.get('interaction-handlers').run(interaction); + } +} diff --git a/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts b/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts new file mode 100644 index 000000000..15e6f83c9 --- /dev/null +++ b/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts @@ -0,0 +1,26 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { fromAsync, isErr } from '../../../lib/parsers/Result'; +import { Listener } from '../../../lib/structures/Listener'; +import { ChatInputCommandAcceptedPayload, Events } from '../../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.ChatInputCommandAccepted }); + } + + public async run(payload: ChatInputCommandAcceptedPayload) { + const { command, context, interaction } = payload; + + const result = await fromAsync(async () => { + this.container.client.emit(Events.ChatInputCommandRun, interaction, command, { ...payload }); + const result = await command.chatInputRun(interaction, context); + this.container.client.emit(Events.ChatInputCommandSuccess, { ...payload, result }); + }); + + if (isErr(result)) { + this.container.client.emit(Events.ChatInputCommandError, result.error, { ...payload }); + } + + this.container.client.emit(Events.ChatInputCommandFinish, interaction, command, { ...payload }); + } +} diff --git a/src/listeners/application-commands/chat-input/CorePossibleChatInputCommand.ts b/src/listeners/application-commands/chat-input/CorePossibleChatInputCommand.ts new file mode 100644 index 000000000..260efee7f --- /dev/null +++ b/src/listeners/application-commands/chat-input/CorePossibleChatInputCommand.ts @@ -0,0 +1,40 @@ +import type { PieceContext } from '@sapphire/pieces'; +import type { CommandInteraction } from 'discord.js'; +import type { ChatInputCommand } from '../../../lib/structures/Command'; +import { Listener } from '../../../lib/structures/Listener'; +import { Events } from '../../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.PossibleChatInputCommand }); + } + + public run(interaction: CommandInteraction) { + const { client, stores } = this.container; + const commandStore = stores.get('commands'); + + const command = commandStore.get(interaction.commandId) ?? commandStore.get(interaction.commandName); + if (!command) { + client.emit(Events.UnknownChatInputCommand, { + interaction, + context: { commandId: interaction.commandId, commandName: interaction.commandName } + }); + return; + } + + if (!command.chatInputRun) { + client.emit(Events.CommandDoesNotHaveChatInputCommandHandler, { + command, + interaction, + context: { commandId: interaction.commandId, commandName: interaction.commandName } + }); + return; + } + + client.emit(Events.PreChatInputCommandRun, { + command: command as ChatInputCommand, + context: { commandId: interaction.commandId, commandName: interaction.commandName }, + interaction + }); + } +} diff --git a/src/listeners/application-commands/chat-input/CorePreChatInputCommandRun.ts b/src/listeners/application-commands/chat-input/CorePreChatInputCommandRun.ts new file mode 100644 index 000000000..80c1cc484 --- /dev/null +++ b/src/listeners/application-commands/chat-input/CorePreChatInputCommandRun.ts @@ -0,0 +1,29 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../../lib/structures/Listener'; +import { Events, PreChatInputCommandRunPayload } from '../../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.PreChatInputCommandRun }); + } + + public async run(payload: PreChatInputCommandRunPayload) { + const { command, interaction } = payload; + + // Run global preconditions: + const globalResult = await this.container.stores.get('preconditions').chatInputRun(interaction, command, payload as any); + if (!globalResult.success) { + this.container.client.emit(Events.ChatInputCommandDenied, globalResult.error, payload); + return; + } + + // Run command-specific preconditions: + const localResult = await command.preconditions.chatInputRun(interaction, command, payload as any); + if (!localResult.success) { + this.container.client.emit(Events.ChatInputCommandDenied, localResult.error, payload); + return; + } + + this.container.client.emit(Events.ChatInputCommandAccepted, payload); + } +} diff --git a/src/listeners/application-commands/context-menu/CoreContextMenuCommandAccepted.ts b/src/listeners/application-commands/context-menu/CoreContextMenuCommandAccepted.ts new file mode 100644 index 000000000..1d9cf39b2 --- /dev/null +++ b/src/listeners/application-commands/context-menu/CoreContextMenuCommandAccepted.ts @@ -0,0 +1,26 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { fromAsync, isErr } from '../../../lib/parsers/Result'; +import { Listener } from '../../../lib/structures/Listener'; +import { ContextMenuCommandAcceptedPayload, Events } from '../../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.ContextMenuCommandAccepted }); + } + + public async run(payload: ContextMenuCommandAcceptedPayload) { + const { command, context, interaction } = payload; + + const result = await fromAsync(async () => { + this.container.client.emit(Events.ContextMenuCommandRun, interaction, command, { ...payload }); + const result = await command.contextMenuRun(interaction, context); + this.container.client.emit(Events.ContextMenuCommandSuccess, { ...payload, result }); + }); + + if (isErr(result)) { + this.container.client.emit(Events.ContextMenuCommandError, result.error, { ...payload }); + } + + this.container.client.emit(Events.ContextMenuCommandFinish, interaction, command, { ...payload }); + } +} diff --git a/src/listeners/application-commands/context-menu/CorePossibleContextMenuCommand.ts b/src/listeners/application-commands/context-menu/CorePossibleContextMenuCommand.ts new file mode 100644 index 000000000..76f460c1c --- /dev/null +++ b/src/listeners/application-commands/context-menu/CorePossibleContextMenuCommand.ts @@ -0,0 +1,40 @@ +import type { PieceContext } from '@sapphire/pieces'; +import type { ContextMenuInteraction } from 'discord.js'; +import type { ContextMenuCommand } from '../../../lib/structures/Command'; +import { Listener } from '../../../lib/structures/Listener'; +import { Events } from '../../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.PossibleContextMenuCommand }); + } + + public run(interaction: ContextMenuInteraction) { + const { client, stores } = this.container; + const commandStore = stores.get('commands'); + + const command = commandStore.get(interaction.commandId) ?? commandStore.get(interaction.commandName); + if (!command) { + client.emit(Events.UnknownContextMenuCommand, { + interaction, + context: { commandId: interaction.commandId, commandName: interaction.commandName } + }); + return; + } + + if (!command.contextMenuRun) { + client.emit(Events.CommandDoesNotHaveContextMenuCommandHandler, { + command, + interaction, + context: { commandId: interaction.commandId, commandName: interaction.commandName } + }); + return; + } + + client.emit(Events.PreContextMenuCommandRun, { + command: command as ContextMenuCommand, + context: { commandId: interaction.commandId, commandName: interaction.commandName }, + interaction + }); + } +} diff --git a/src/listeners/application-commands/context-menu/CorePreContextMenuCommandRun.ts b/src/listeners/application-commands/context-menu/CorePreContextMenuCommandRun.ts new file mode 100644 index 000000000..fb4f7a9fc --- /dev/null +++ b/src/listeners/application-commands/context-menu/CorePreContextMenuCommandRun.ts @@ -0,0 +1,29 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../../lib/structures/Listener'; +import { Events, PreContextMenuCommandRunPayload } from '../../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.PreContextMenuCommandRun }); + } + + public async run(payload: PreContextMenuCommandRunPayload) { + const { command, interaction } = payload; + + // Run global preconditions: + const globalResult = await this.container.stores.get('preconditions').contextMenuRun(interaction, command, payload as any); + if (!globalResult.success) { + this.container.client.emit(Events.ContextMenuCommandDenied, globalResult.error, payload); + return; + } + + // Run command-specific preconditions: + const localResult = await command.preconditions.contextMenuRun(interaction, command, payload as any); + if (!localResult.success) { + this.container.client.emit(Events.ContextMenuCommandDenied, localResult.error, payload); + return; + } + + this.container.client.emit(Events.ContextMenuCommandAccepted, payload); + } +} diff --git a/src/listeners/command-handler/CoreCommandAccepted.ts b/src/listeners/command-handler/CoreCommandAccepted.ts deleted file mode 100644 index 68c4f4ada..000000000 --- a/src/listeners/command-handler/CoreCommandAccepted.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { PieceContext } from '@sapphire/pieces'; -import { fromAsync, isErr } from '../../lib/parsers/Result'; -import { Listener } from '../../lib/structures/Listener'; -import { CommandAcceptedPayload, Events } from '../../lib/types/Events'; - -export class CoreListener extends Listener { - public constructor(context: PieceContext) { - super(context, { event: Events.CommandAccepted }); - } - - public async run(payload: CommandAcceptedPayload) { - const { message, command, parameters, context } = payload; - const args = await command.preParse(message, parameters, context); - const result = await fromAsync(async () => { - message.client.emit(Events.CommandRun, message, command, { ...payload, args }); - const result = await command.messageRun(message, args, context); - message.client.emit(Events.CommandSuccess, { ...payload, args, result }); - }); - - if (isErr(result)) { - message.client.emit(Events.CommandError, result.error, { ...payload, args, piece: command }); - } - - message.client.emit(Events.CommandFinish, message, command, { ...payload, args }); - } -} diff --git a/src/listeners/command-handler/CoreCommandTyping.ts b/src/listeners/command-handler/CoreCommandTyping.ts deleted file mode 100644 index 699aba7c5..000000000 --- a/src/listeners/command-handler/CoreCommandTyping.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { PieceContext } from '@sapphire/pieces'; -import type { Message } from 'discord.js'; -import type { Command } from '../../lib/structures/Command'; -import { Listener } from '../../lib/structures/Listener'; -import { CommandRunPayload, Events } from '../../lib/types/Events'; - -export class CoreListener extends Listener { - public constructor(context: PieceContext) { - super(context, { event: Events.CommandRun }); - this.enabled = this.container.client.options.typing ?? false; - } - - public async run(message: Message, command: Command, payload: CommandRunPayload) { - if (!command.typing) return; - - try { - await message.channel.sendTyping(); - } catch (error) { - message.client.emit(Events.CommandTypingError, error, { ...payload, command, message }); - } - } -} diff --git a/src/listeners/command-handler/CorePreCommandRun.ts b/src/listeners/command-handler/CorePreCommandRun.ts deleted file mode 100644 index 5e21d2e54..000000000 --- a/src/listeners/command-handler/CorePreCommandRun.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { PieceContext } from '@sapphire/pieces'; -import { Listener } from '../../lib/structures/Listener'; -import { Events, PreCommandRunPayload } from '../../lib/types/Events'; - -export class CoreListener extends Listener { - public constructor(context: PieceContext) { - super(context, { event: Events.PreCommandRun }); - } - - public async run(payload: PreCommandRunPayload) { - const { message, command } = payload; - - // Run global preconditions: - const globalResult = await this.container.stores.get('preconditions').run(message, command, payload as any); - if (!globalResult.success) { - message.client.emit(Events.CommandDenied, globalResult.error, payload); - return; - } - - // Run command-specific preconditions: - const localResult = await command.preconditions.run(message, command, payload as any); - if (!localResult.success) { - message.client.emit(Events.CommandDenied, localResult.error, payload); - return; - } - - message.client.emit(Events.CommandAccepted, payload); - } -} diff --git a/src/optional-listeners/error-listeners/CoreChatInputCommandError.ts b/src/optional-listeners/error-listeners/CoreChatInputCommandError.ts new file mode 100644 index 000000000..8b67c9395 --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreChatInputCommandError.ts @@ -0,0 +1,14 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { ChatInputCommandErrorPayload, Events } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.ChatInputCommandError }); + } + + public run(error: unknown, context: ChatInputCommandErrorPayload) { + const { name, location } = context.command; + this.container.logger.error(`Encountered error on chat input command "${name}" at path "${location.full}"`, error); + } +} diff --git a/src/optional-listeners/error-listeners/CoreCommandApplicationCommandRegistryError.ts b/src/optional-listeners/error-listeners/CoreCommandApplicationCommandRegistryError.ts new file mode 100644 index 000000000..b55c07e9a --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreCommandApplicationCommandRegistryError.ts @@ -0,0 +1,18 @@ +import type { PieceContext } from '@sapphire/pieces'; +import type { Command } from '../../lib/structures/Command'; +import { Listener } from '../../lib/structures/Listener'; +import { Events } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.CommandApplicationCommandRegistryError }); + } + + public run(error: unknown, command: Command) { + const { name, location } = command; + this.container.logger.error( + `Encountered error while handling the command application command registry for command "${name}" at path "${location.full}"`, + error + ); + } +} diff --git a/src/optional-listeners/error-listeners/CoreCommandAutocompleteInteractionError.ts b/src/optional-listeners/error-listeners/CoreCommandAutocompleteInteractionError.ts new file mode 100644 index 000000000..8810ab5b5 --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreCommandAutocompleteInteractionError.ts @@ -0,0 +1,17 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { AutocompleteInteractionPayload, Events } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.CommandAutocompleteInteractionError }); + } + + public run(error: unknown, context: AutocompleteInteractionPayload) { + const { name, location } = context.command; + this.container.logger.error( + `Encountered error while handling an autocomplete run method on command "${name}" at path "${location.full}"`, + error + ); + } +} diff --git a/src/optional-listeners/error-listeners/CoreContextMenuCommandError.ts b/src/optional-listeners/error-listeners/CoreContextMenuCommandError.ts new file mode 100644 index 000000000..1021d8be4 --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreContextMenuCommandError.ts @@ -0,0 +1,14 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { ContextMenuCommandErrorPayload, Events } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.ContextMenuCommandError }); + } + + public run(error: unknown, context: ContextMenuCommandErrorPayload) { + const { name, location } = context.command; + this.container.logger.error(`Encountered error on message command "${name}" at path "${location.full}"`, error); + } +} diff --git a/src/optional-listeners/error-listeners/CoreInteractionHandlerError.ts b/src/optional-listeners/error-listeners/CoreInteractionHandlerError.ts new file mode 100644 index 000000000..c6a2d3951 --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreInteractionHandlerError.ts @@ -0,0 +1,17 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { Events, InteractionHandlerError } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.InteractionHandlerError }); + } + + public run(error: unknown, context: InteractionHandlerError) { + const { name, location } = context.handler; + this.container.logger.error( + `Encountered error while handling an interaction handler run method for interaction-handler "${name}" at path "${location.full}"`, + error + ); + } +} diff --git a/src/optional-listeners/error-listeners/CoreInteractionHandlerParseError.ts b/src/optional-listeners/error-listeners/CoreInteractionHandlerParseError.ts new file mode 100644 index 000000000..b965b72e4 --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreInteractionHandlerParseError.ts @@ -0,0 +1,17 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { Events, InteractionHandlerParseError as InteractionHandlerParseErrorPayload } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.InteractionHandlerParseError }); + } + + public run(error: unknown, context: InteractionHandlerParseErrorPayload) { + const { name, location } = context.handler; + this.container.logger.error( + `Encountered error while handling an interaction handler parse method for interaction-handler "${name}" at path "${location.full}"`, + error + ); + } +} diff --git a/src/errorListeners/CoreEventError.ts b/src/optional-listeners/error-listeners/CoreListenerError.ts similarity index 56% rename from src/errorListeners/CoreEventError.ts rename to src/optional-listeners/error-listeners/CoreListenerError.ts index 717746830..45a6c050e 100644 --- a/src/errorListeners/CoreEventError.ts +++ b/src/optional-listeners/error-listeners/CoreListenerError.ts @@ -1,14 +1,14 @@ import type { PieceContext } from '@sapphire/pieces'; -import { Listener } from '../lib/structures/Listener'; -import { Events, ListenerErrorPayload } from '../lib/types/Events'; +import { Listener } from '../../lib/structures/Listener'; +import { ListenerErrorPayload, Events } from '../../lib/types/Events'; export class CoreEvent extends Listener { public constructor(context: PieceContext) { super(context, { event: Events.ListenerError }); } - public run(error: Error, context: ListenerErrorPayload) { + public run(error: unknown, context: ListenerErrorPayload) { const { name, event, location } = context.piece; - this.container.logger.error(`Encountered error on event listener "${name}" for event "${event}" at path "${location.full}"`, error); + this.container.logger.error(`Encountered error on event listener "${name}" for event "${String(event)}" at path "${location.full}"`, error); } } diff --git a/src/optional-listeners/error-listeners/CoreMessageCommandError.ts b/src/optional-listeners/error-listeners/CoreMessageCommandError.ts new file mode 100644 index 000000000..bfad8170c --- /dev/null +++ b/src/optional-listeners/error-listeners/CoreMessageCommandError.ts @@ -0,0 +1,14 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { MessageCommandErrorPayload, Events } from '../../lib/types/Events'; + +export class CoreEvent extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.MessageCommandError }); + } + + public run(error: unknown, context: MessageCommandErrorPayload) { + const { name, location } = context.command; + this.container.logger.error(`Encountered error on message command "${name}" at path "${location.full}"`, error); + } +} diff --git a/src/optional-listeners/message-command-listeners/CoreMessageCommandAccepted.ts b/src/optional-listeners/message-command-listeners/CoreMessageCommandAccepted.ts new file mode 100644 index 000000000..861040ea9 --- /dev/null +++ b/src/optional-listeners/message-command-listeners/CoreMessageCommandAccepted.ts @@ -0,0 +1,26 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { MessageCommandAcceptedPayload, Events } from '../../lib/types/Events'; +import { isErr, fromAsync } from '../../lib/parsers/Result'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.MessageCommandAccepted }); + } + + public async run(payload: MessageCommandAcceptedPayload) { + const { message, command, parameters, context } = payload; + const args = await command.messagePreParse(message, parameters, context); + const result = await fromAsync(async () => { + message.client.emit(Events.MessageCommandRun, message, command, { ...payload, args }); + const result = await command.messageRun(message, args, context); + message.client.emit(Events.MessageCommandSuccess, { ...payload, args, result }); + }); + + if (isErr(result)) { + message.client.emit(Events.MessageCommandError, result.error, { ...payload, args }); + } + + message.client.emit(Events.MessageCommandFinish, message, command, { ...payload, args }); + } +} diff --git a/src/optional-listeners/message-command-listeners/CoreMessageCommandTyping.ts b/src/optional-listeners/message-command-listeners/CoreMessageCommandTyping.ts new file mode 100644 index 000000000..c61c778a3 --- /dev/null +++ b/src/optional-listeners/message-command-listeners/CoreMessageCommandTyping.ts @@ -0,0 +1,22 @@ +import type { MessageCommand } from '../../lib/structures/Command'; +import type { Message } from 'discord.js'; +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { MessageCommandRunPayload, Events } from '../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.MessageCommandRun }); + this.enabled = this.container.client.options.typing ?? false; + } + + public async run(message: Message, command: MessageCommand, payload: MessageCommandRunPayload) { + if (!command.typing) return; + + try { + await message.channel.sendTyping(); + } catch (error) { + message.client.emit(Events.MessageCommandTypingError, error as Error, { ...payload, command, message }); + } + } +} diff --git a/src/listeners/command-handler/CoreMessage.ts b/src/optional-listeners/message-command-listeners/CoreMessageCreate.ts similarity index 100% rename from src/listeners/command-handler/CoreMessage.ts rename to src/optional-listeners/message-command-listeners/CoreMessageCreate.ts diff --git a/src/optional-listeners/message-command-listeners/CorePreMessageCommandRun.ts b/src/optional-listeners/message-command-listeners/CorePreMessageCommandRun.ts new file mode 100644 index 000000000..d6879ee51 --- /dev/null +++ b/src/optional-listeners/message-command-listeners/CorePreMessageCommandRun.ts @@ -0,0 +1,29 @@ +import type { PieceContext } from '@sapphire/pieces'; +import { Listener } from '../../lib/structures/Listener'; +import { Events, PreMessageCommandRunPayload } from '../../lib/types/Events'; + +export class CoreListener extends Listener { + public constructor(context: PieceContext) { + super(context, { event: Events.PreMessageCommandRun }); + } + + public async run(payload: PreMessageCommandRunPayload) { + const { message, command } = payload; + + // Run global preconditions: + const globalResult = await this.container.stores.get('preconditions').messageRun(message, command, payload as any); + if (!globalResult.success) { + message.client.emit(Events.MessageCommandDenied, globalResult.error, payload); + return; + } + + // Run command-specific preconditions: + const localResult = await command.preconditions.messageRun(message, command, payload as any); + if (!localResult.success) { + message.client.emit(Events.MessageCommandDenied, localResult.error, payload); + return; + } + + message.client.emit(Events.MessageCommandAccepted, payload); + } +} diff --git a/src/listeners/command-handler/CoreMessageParser.ts b/src/optional-listeners/message-command-listeners/CorePreMessageParser.ts similarity index 90% rename from src/listeners/command-handler/CoreMessageParser.ts rename to src/optional-listeners/message-command-listeners/CorePreMessageParser.ts index 6d3926f77..c47a1b7b3 100644 --- a/src/listeners/command-handler/CoreMessageParser.ts +++ b/src/optional-listeners/message-command-listeners/CorePreMessageParser.ts @@ -6,6 +6,7 @@ import { Events } from '../../lib/types/Events'; export class CoreListener extends Listener { private readonly requiredPermissions = new Permissions(['VIEW_CHANNEL', 'SEND_MESSAGES']).freeze(); + public constructor(context: PieceContext) { super(context, { event: Events.PreMessageParsed }); } @@ -19,6 +20,7 @@ export class CoreListener extends Listener { const mentionPrefix = this.getMentionPrefix(message); const { client } = this.container; const { regexPrefix } = client.options; + if (mentionPrefix) { if (message.content.length === mentionPrefix.length) { client.emit(Events.MentionPrefixOnly, message); @@ -45,7 +47,7 @@ export class CoreListener extends Listener { if (!me) return false; const channel = message.channel as GuildBasedChannelTypes; - return channel.permissionsFor(me).has(this.requiredPermissions, false); + return channel.permissionsFor(me).has(this.requiredPermissions, true); } private getMentionPrefix(message: Message): string | null { @@ -56,17 +58,19 @@ export class CoreListener extends Listener { // Calculate the offset and the ID that is being provided const [offset, id] = message.content[2] === '&' - ? [3, message.guild?.roles.botRoleFor(message.guild.me!)?.id] + ? [3, message.guild?.roles.botRoleFor(this.container.client.id!)?.id] : [message.content[2] === '!' ? 3 : 2, this.container.client.id]; if (!id) return null; + const offsetWithId = offset + id.length; + // If the mention doesn't end with `>`, skip early: - if (message.content[offset + id.length] !== '>') return null; + if (message.content[offsetWithId] !== '>') return null; // Check whether or not the ID is the same as the managed role ID: - const mentionId = message.content.substr(offset, id.length); - if (mentionId === id) return message.content.substr(0, offset + id.length + 1); + const mentionId = message.content.substring(offset, offsetWithId); + if (mentionId === id) return message.content.substring(0, offsetWithId + 1); return null; } diff --git a/src/listeners/command-handler/CorePrefixedMessage.ts b/src/optional-listeners/message-command-listeners/CorePrefixedMessage.ts similarity index 64% rename from src/listeners/command-handler/CorePrefixedMessage.ts rename to src/optional-listeners/message-command-listeners/CorePrefixedMessage.ts index 891554084..98dfd7555 100644 --- a/src/listeners/command-handler/CorePrefixedMessage.ts +++ b/src/optional-listeners/message-command-listeners/CorePrefixedMessage.ts @@ -1,5 +1,6 @@ import type { PieceContext } from '@sapphire/pieces'; import type { Message } from 'discord.js'; +import type { MessageCommand } from '../../lib/structures/Command'; import { Listener } from '../../lib/structures/Listener'; import { Events } from '../../lib/types/Events'; @@ -19,20 +20,31 @@ export class CoreListener extends Listener { const spaceIndex = prefixLess.indexOf(' '); const commandName = spaceIndex === -1 ? prefixLess : prefixLess.slice(0, spaceIndex); if (commandName.length === 0) { - client.emit(Events.UnknownCommandName, { message, prefix, commandPrefix }); + client.emit(Events.UnknownMessageCommandName, { message, prefix, commandPrefix }); return; } // Retrieve the command and validate: const command = stores.get('commands').get(client.options.caseInsensitiveCommands ? commandName.toLowerCase() : commandName); if (!command) { - client.emit(Events.UnknownCommand, { message, prefix, commandName, commandPrefix }); + client.emit(Events.UnknownMessageCommand, { message, prefix, commandName, commandPrefix }); + return; + } + + // If the command exists but is missing a message handler, emit a different event (maybe an application command variant exists) + if (!command.messageRun) { + client.emit(Events.CommandDoesNotHaveMessageCommandHandler, { message, prefix, commandPrefix, command }); return; } // Run the last stage before running the command: - const parameters = spaceIndex === -1 ? '' : prefixLess.substr(spaceIndex + 1).trim(); - client.emit(Events.PreCommandRun, { message, command, parameters, context: { commandName, commandPrefix, prefix } }); + const parameters = spaceIndex === -1 ? '' : prefixLess.substring(spaceIndex + 1).trim(); + client.emit(Events.PreMessageCommandRun, { + message, + command: command as MessageCommand, + parameters, + context: { commandName, commandPrefix, prefix } + }); } private getCommandPrefix(content: string, prefix: string | RegExp): string { diff --git a/src/preconditions/ClientPermissions.ts b/src/preconditions/ClientPermissions.ts index ff97ac614..26f5556b8 100644 --- a/src/preconditions/ClientPermissions.ts +++ b/src/preconditions/ClientPermissions.ts @@ -1,9 +1,21 @@ -import { Message, NewsChannel, Permissions, PermissionString, TextChannel } from 'discord.js'; +import { + BaseGuildTextChannel, + CommandInteraction, + ContextMenuInteraction, + GuildTextBasedChannel, + Message, + Permissions, + PermissionString +} from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import type { Command } from '../lib/structures/Command'; -import { Precondition, PreconditionContext, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition, PreconditionContext } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { +export interface UserPermissionsPreconditionContext extends PreconditionContext { + permissions?: Permissions; +} + +export class CorePrecondition extends AllFlowsPrecondition { private readonly dmChannelPermissions = new Permissions( ~new Permissions([ // @@ -17,9 +29,9 @@ export class CorePrecondition extends Precondition { ]).bitfield & Permissions.ALL ).freeze(); - public run(message: Message, _command: Command, context: PreconditionContext): PreconditionResult { - const required = (context.permissions as Permissions) ?? new Permissions(); - const channel = message.channel as TextChannel | NewsChannel; + public messageRun(message: Message, _: Command, context: UserPermissionsPreconditionContext) { + const required = context.permissions ?? new Permissions(); + const channel = message.channel as BaseGuildTextChannel; if (!message.client.id) { return this.error({ @@ -30,14 +42,37 @@ export class CorePrecondition extends Precondition { const permissions = message.guild ? channel.permissionsFor(message.client.id) : this.dmChannelPermissions; - if (!permissions) { + return this.sharedRun(required, permissions, 'message'); + } + + public async chatInputRun(interaction: CommandInteraction, _: Command, context: UserPermissionsPreconditionContext) { + const required = context.permissions ?? new Permissions(); + + const channel = (await this.fetchChannelFromInteraction(interaction)) as GuildTextBasedChannel; + + const permissions = interaction.inGuild() ? channel.permissionsFor(interaction.applicationId) : this.dmChannelPermissions; + + return this.sharedRun(required, permissions, 'chat input'); + } + + public async contextMenuRun(interaction: ContextMenuInteraction, _: Command, context: UserPermissionsPreconditionContext) { + const required = context.permissions ?? new Permissions(); + const channel = (await this.fetchChannelFromInteraction(interaction)) as GuildTextBasedChannel; + + const permissions = interaction.inGuild() ? channel.permissionsFor(interaction.applicationId) : this.dmChannelPermissions; + + return this.sharedRun(required, permissions, 'context menu'); + } + + private sharedRun(requiredPermissions: Permissions, availablePermissions: Permissions | null, commandType: string) { + if (!availablePermissions) { return this.error({ identifier: Identifiers.PreconditionClientPermissionsNoPermissions, - message: 'I was unable to resolve my permissions in the command invocation channel.' + message: `I was unable to resolve my permissions in the ${commandType} command invocation channel.` }); } - const missing = permissions.missing(required); + const missing = availablePermissions.missing(requiredPermissions); return missing.length === 0 ? this.ok() : this.error({ diff --git a/src/preconditions/Cooldown.ts b/src/preconditions/Cooldown.ts index e1437262b..8ff207d5d 100644 --- a/src/preconditions/Cooldown.ts +++ b/src/preconditions/Cooldown.ts @@ -1,8 +1,8 @@ import { RateLimitManager } from '@sapphire/ratelimits'; -import type { Message, Snowflake } from 'discord.js'; +import type { BaseCommandInteraction, CommandInteraction, ContextMenuInteraction, Message, Snowflake } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import type { Command } from '../lib/structures/Command'; -import { Precondition, PreconditionContext } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition, PreconditionContext } from '../lib/structures/Precondition'; import { BucketScope } from '../lib/types/Enums'; export interface CooldownContext extends PreconditionContext { @@ -12,27 +12,47 @@ export interface CooldownContext extends PreconditionContext { filteredUsers?: Snowflake[]; } -export class CorePrecondition extends Precondition { +export class CorePrecondition extends AllFlowsPrecondition { public buckets = new WeakMap>(); - public run(message: Message, command: Command, context: CooldownContext) { + public messageRun(message: Message, command: Command, context: CooldownContext) { + const cooldownId = this.getIdFromMessage(message, context); + + return this.sharedRun(message.author.id, command, context, cooldownId, 'message'); + } + + public chatInputRun(interaction: CommandInteraction, command: Command, context: CooldownContext) { + const cooldownId = this.getIdFromInteraction(interaction, context); + + return this.sharedRun(interaction.user.id, command, context, cooldownId, 'chat input'); + } + + public contextMenuRun(interaction: ContextMenuInteraction, command: Command, context: CooldownContext) { + const cooldownId = this.getIdFromInteraction(interaction, context); + + return this.sharedRun(interaction.user.id, command, context, cooldownId, 'context menu'); + } + + private sharedRun(authorId: string, command: Command, context: CooldownContext, cooldownId: string, commandType: string) { // If the command it is testing for is not this one, return ok: if (context.external) return this.ok(); // If there is no delay (undefined, null, 0), return ok: if (!context.delay) return this.ok(); - // If the user has provided any filtered users and the message author is in that array, return ok: - if (context.filteredUsers?.includes(message.author.id)) return this.ok(); + // If the user has provided any filtered users and the authorId is in that array, return ok: + if (context.filteredUsers?.includes(authorId)) return this.ok(); + + const ratelimit = this.getManager(command, context).acquire(cooldownId); - const ratelimit = this.getManager(command, context).acquire(this.getId(message, context)); if (ratelimit.limited) { const remaining = ratelimit.remainingTime; + return this.error({ identifier: Identifiers.PreconditionCooldown, - message: `There is a cooldown in effect for this command. It can be used again in ${Math.ceil(remaining / 1000)} second${ - remaining > 1000 ? 's' : '' - }.`, + message: `There is a cooldown in effect for this ${commandType} command. It'll be available at ${new Date( + ratelimit.expires + ).toISOString()}.`, context: { remaining } }); } @@ -41,7 +61,7 @@ export class CorePrecondition extends Precondition { return this.ok(); } - private getId(message: Message, context: CooldownContext) { + private getIdFromMessage(message: Message, context: CooldownContext) { switch (context.scope) { case BucketScope.Global: return 'global'; @@ -54,6 +74,19 @@ export class CorePrecondition extends Precondition { } } + private getIdFromInteraction(interaction: BaseCommandInteraction, context: CooldownContext) { + switch (context.scope) { + case BucketScope.Global: + return 'global'; + case BucketScope.Channel: + return interaction.channelId; + case BucketScope.Guild: + return interaction.guildId ?? interaction.channelId; + default: + return interaction.user.id; + } + } + private getManager(command: Command, context: CooldownContext) { let manager = this.buckets.get(command); if (!manager) { diff --git a/src/preconditions/DMOnly.ts b/src/preconditions/DMOnly.ts index 836c95102..b860769c4 100644 --- a/src/preconditions/DMOnly.ts +++ b/src/preconditions/DMOnly.ts @@ -1,11 +1,23 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { return message.guild === null ? this.ok() - : this.error({ identifier: Identifiers.PreconditionDMOnly, message: 'You cannot run this command outside DMs.' }); + : this.error({ identifier: Identifiers.PreconditionDMOnly, message: 'You cannot run this message command outside DMs.' }); + } + + public chatInputRun(interaction: CommandInteraction) { + return interaction.guildId === null + ? this.ok() + : this.error({ identifier: Identifiers.PreconditionDMOnly, message: 'You cannot run this chat input command outside DMs.' }); + } + + public contextMenuRun(interaction: ContextMenuInteraction) { + return interaction.guildId === null + ? this.ok() + : this.error({ identifier: Identifiers.PreconditionDMOnly, message: 'You cannot run this context menu command outside DMs.' }); } } diff --git a/src/preconditions/Enabled.ts b/src/preconditions/Enabled.ts index da06c66f5..0948a2be9 100644 --- a/src/preconditions/Enabled.ts +++ b/src/preconditions/Enabled.ts @@ -1,15 +1,29 @@ import type { PieceContext } from '@sapphire/pieces'; -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import type { Command } from '../lib/structures/Command'; -import { Precondition } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { +export class CorePrecondition extends AllFlowsPrecondition { public constructor(context: PieceContext) { super(context, { position: 10 }); } - public run(_: Message, command: Command, context: Precondition.Context): Precondition.Result { - return command.enabled ? this.ok() : this.error({ identifier: Identifiers.CommandDisabled, message: 'This command is disabled.', context }); + public messageRun(_: Message, command: Command, context: AllFlowsPrecondition.Context) { + return command.enabled + ? this.ok() + : this.error({ identifier: Identifiers.CommandDisabled, message: 'This message command is disabled.', context }); + } + + public chatInputRun(_: CommandInteraction, command: Command, context: AllFlowsPrecondition.Context) { + return command.enabled + ? this.ok() + : this.error({ identifier: Identifiers.CommandDisabled, message: 'This chat input command is disabled.', context }); + } + + public contextMenuRun(_: ContextMenuInteraction, command: Command, context: AllFlowsPrecondition.Context) { + return command.enabled + ? this.ok() + : this.error({ identifier: Identifiers.CommandDisabled, message: 'This context menu command is disabled.', context }); } } diff --git a/src/preconditions/GuildNewsOnly.ts b/src/preconditions/GuildNewsOnly.ts index 6bafb5c3a..73fc8951b 100644 --- a/src/preconditions/GuildNewsOnly.ts +++ b/src/preconditions/GuildNewsOnly.ts @@ -1,16 +1,38 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message, TextBasedChannelTypes } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - private readonly allowedTypes: Message['channel']['type'][] = ['GUILD_NEWS', 'GUILD_NEWS_THREAD']; +export class CorePrecondition extends AllFlowsPrecondition { + private readonly allowedTypes: TextBasedChannelTypes[] = ['GUILD_NEWS', 'GUILD_NEWS_THREAD']; - public run(message: Message): PreconditionResult { + public messageRun(message: Message) { return this.allowedTypes.includes(message.channel.type) ? this.ok() : this.error({ identifier: Identifiers.PreconditionGuildNewsOnly, - message: 'You can only run this command in server announcement channels.' + message: 'You can only run this message command in server announcement channels.' + }); + } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return this.allowedTypes.includes(channel.type) + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildNewsOnly, + message: 'You can only run this chat input command in server announcement channels.' + }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return this.allowedTypes.includes(channel.type) + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildNewsOnly, + message: 'You can only run this context menu command in server announcement channels.' }); } } diff --git a/src/preconditions/GuildNewsThreadOnly.ts b/src/preconditions/GuildNewsThreadOnly.ts index 23cb58169..3c5205a6c 100644 --- a/src/preconditions/GuildNewsThreadOnly.ts +++ b/src/preconditions/GuildNewsThreadOnly.ts @@ -1,14 +1,36 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { return message.thread?.type === 'GUILD_NEWS_THREAD' ? this.ok() : this.error({ identifier: Identifiers.PreconditionGuildNewsThreadOnly, - message: 'You can only run this command in server announcement thread channels.' + message: 'You can only run this message command in server announcement thread channels.' + }); + } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.type === 'GUILD_NEWS_THREAD' + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildNewsThreadOnly, + message: 'You can only run this chat input command in server announcement thread channels.' + }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.type === 'GUILD_NEWS_THREAD' + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildNewsThreadOnly, + message: 'You can only run this context menu command in server announcement thread channels.' }); } } diff --git a/src/preconditions/GuildOnly.ts b/src/preconditions/GuildOnly.ts index 075aa4f59..c75534188 100644 --- a/src/preconditions/GuildOnly.ts +++ b/src/preconditions/GuildOnly.ts @@ -1,11 +1,23 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { - return message.guild === null - ? this.error({ identifier: Identifiers.PreconditionGuildOnly, message: 'You cannot run this command in DMs.' }) +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { + return message.guildId === null + ? this.error({ identifier: Identifiers.PreconditionGuildOnly, message: 'You cannot run this message command in DMs.' }) + : this.ok(); + } + + public chatInputRun(interaction: CommandInteraction) { + return interaction.guildId === null + ? this.error({ identifier: Identifiers.PreconditionGuildOnly, message: 'You cannot run this chat input command in DMs.' }) + : this.ok(); + } + + public contextMenuRun(interaction: ContextMenuInteraction) { + return interaction.guildId === null + ? this.error({ identifier: Identifiers.PreconditionGuildOnly, message: 'You cannot run this context menu command in DMs.' }) : this.ok(); } } diff --git a/src/preconditions/GuildPrivateThreadOnly.ts b/src/preconditions/GuildPrivateThreadOnly.ts index 0ed4ef89c..d721e7bd2 100644 --- a/src/preconditions/GuildPrivateThreadOnly.ts +++ b/src/preconditions/GuildPrivateThreadOnly.ts @@ -1,9 +1,9 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { return message.thread?.type === 'GUILD_PRIVATE_THREAD' ? this.ok() : this.error({ @@ -11,4 +11,26 @@ export class CorePrecondition extends Precondition { message: 'You can only run this command in private server thread channels.' }); } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.type === 'GUILD_PRIVATE_THREAD' + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildPrivateThreadOnly, + message: 'You can only run this chat input command in private server thread channels.' + }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.type === 'GUILD_PRIVATE_THREAD' + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildPrivateThreadOnly, + message: 'You can only run this context menu command in private server thread channels.' + }); + } } diff --git a/src/preconditions/GuildPublicThreadOnly.ts b/src/preconditions/GuildPublicThreadOnly.ts index 1f72f87f7..906b3a3b7 100644 --- a/src/preconditions/GuildPublicThreadOnly.ts +++ b/src/preconditions/GuildPublicThreadOnly.ts @@ -1,14 +1,36 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { return message.thread?.type === 'GUILD_PUBLIC_THREAD' ? this.ok() : this.error({ identifier: Identifiers.PreconditionGuildPublicThreadOnly, - message: 'You can only run this command in public server thread channels.' + message: 'You can only run this message command in public server thread channels.' + }); + } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.type === 'GUILD_PUBLIC_THREAD' + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildPublicThreadOnly, + message: 'You can only run this chat input command in public server thread channels.' + }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.type === 'GUILD_PUBLIC_THREAD' + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildPublicThreadOnly, + message: 'You can only run this context menu command in public server thread channels.' }); } } diff --git a/src/preconditions/GuildTextOnly.ts b/src/preconditions/GuildTextOnly.ts index 1ce01ad41..aa20985ef 100644 --- a/src/preconditions/GuildTextOnly.ts +++ b/src/preconditions/GuildTextOnly.ts @@ -1,13 +1,38 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message, TextBasedChannelTypes } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - private readonly allowedTypes: Message['channel']['type'][] = ['GUILD_TEXT', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD']; +export class CorePrecondition extends AllFlowsPrecondition { + private readonly allowedTypes: TextBasedChannelTypes[] = ['GUILD_TEXT', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD']; - public run(message: Message): PreconditionResult { + public messageRun(message: Message) { return this.allowedTypes.includes(message.channel.type) ? this.ok() - : this.error({ identifier: Identifiers.PreconditionGuildTextOnly, message: 'You can only run this command in server text channels.' }); + : this.error({ + identifier: Identifiers.PreconditionGuildTextOnly, + message: 'You can only run this message command in server text channels.' + }); + } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return this.allowedTypes.includes(channel.type) + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildTextOnly, + message: 'You can only run this chat input command in server text channels.' + }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return this.allowedTypes.includes(channel.type) + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionGuildTextOnly, + message: 'You can only run this context menu command in server text channels.' + }); } } diff --git a/src/preconditions/GuildThreadOnly.ts b/src/preconditions/GuildThreadOnly.ts index c618c4965..4dd984c75 100644 --- a/src/preconditions/GuildThreadOnly.ts +++ b/src/preconditions/GuildThreadOnly.ts @@ -1,11 +1,36 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { return message.thread ? this.ok() - : this.error({ identifier: Identifiers.PreconditionThreadOnly, message: 'You can only run this command in server thread channels.' }); + : this.error({ + identifier: Identifiers.PreconditionThreadOnly, + message: 'You can only run this message command in server thread channels.' + }); + } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.isThread() + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionThreadOnly, + message: 'You can only run this chat input command in server thread channels.' + }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + return channel.isThread() + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionThreadOnly, + message: 'You can only run this chat input command in server thread channels.' + }); } } diff --git a/src/preconditions/NSFW.ts b/src/preconditions/NSFW.ts index 25af6487f..242274b54 100644 --- a/src/preconditions/NSFW.ts +++ b/src/preconditions/NSFW.ts @@ -1,13 +1,33 @@ -import type { Message } from 'discord.js'; +import type { CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; -import { Precondition, PreconditionResult } from '../lib/structures/Precondition'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; -export class CorePrecondition extends Precondition { - public run(message: Message): PreconditionResult { +export class CorePrecondition extends AllFlowsPrecondition { + public messageRun(message: Message) { // `nsfw` is undefined in DMChannel, doing `=== true` - // will result on it returning`false`. + // will result on it returning `false`. return Reflect.get(message.channel, 'nsfw') === true ? this.ok() - : this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this command outside NSFW channels.' }); + : this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this message command outside NSFW channels.' }); + } + + public async chatInputRun(interaction: CommandInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + // `nsfw` is undefined in DMChannel, doing `=== true` + // will result on it returning `false`. + return Reflect.get(channel, 'nsfw') === true + ? this.ok() + : this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this chat input command outside NSFW channels.' }); + } + + public async contextMenuRun(interaction: ContextMenuInteraction) { + const channel = await this.fetchChannelFromInteraction(interaction); + + // `nsfw` is undefined in DMChannel, doing `=== true` + // will result on it returning `false`. + return Reflect.get(channel, 'nsfw') === true + ? this.ok() + : this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this context menu command outside NSFW channels.' }); } } diff --git a/src/preconditions/UserPermissions.ts b/src/preconditions/UserPermissions.ts index 07a5ae81c..5eb02cbd1 100644 --- a/src/preconditions/UserPermissions.ts +++ b/src/preconditions/UserPermissions.ts @@ -1,10 +1,10 @@ -import { Message, NewsChannel, Permissions, TextChannel } from 'discord.js'; +import { CommandInteraction, ContextMenuInteraction, Message, NewsChannel, Permissions, TextChannel } from 'discord.js'; import { Identifiers } from '../lib/errors/Identifiers'; import type { Command } from '../lib/structures/Command'; -import { Precondition, PreconditionContext, PreconditionResult } from '../lib/structures/Precondition'; -import { CorePrecondition as ClientPermissionsPrecondition } from './ClientPermissions'; +import { AllFlowsPrecondition } from '../lib/structures/Precondition'; +import { CorePrecondition as ClientPrecondition, UserPermissionsPreconditionContext } from './ClientPermissions'; -export class CorePrecondition extends Precondition { +export class CorePrecondition extends AllFlowsPrecondition { private readonly dmChannelPermissions = new Permissions( ~new Permissions([ 'ADD_REACTIONS', @@ -19,26 +19,43 @@ export class CorePrecondition extends Precondition { ]).bitfield & Permissions.ALL ).freeze(); - public run(message: Message, _command: Command, context: PreconditionContext): PreconditionResult { - const required = (context.permissions as Permissions) ?? new Permissions(); + public messageRun(message: Message, _command: Command, context: UserPermissionsPreconditionContext) { + const required = context.permissions ?? new Permissions(); const channel = message.channel as TextChannel | NewsChannel; - const permissions = message.guild ? channel.permissionsFor(message.author) : this.dmChannelPermissions; - if (!permissions) { + return this.sharedRun(required, permissions, 'message'); + } + + public chatInputRun(interaction: CommandInteraction, _command: Command, context: UserPermissionsPreconditionContext) { + const required = context.permissions ?? new Permissions(); + const permissions = interaction.guildId ? interaction.memberPermissions : this.dmChannelPermissions; + + return this.sharedRun(required, permissions, 'chat input'); + } + + public contextMenuRun(interaction: ContextMenuInteraction, _command: Command, context: UserPermissionsPreconditionContext) { + const required = context.permissions ?? new Permissions(); + const permissions = interaction.guildId ? interaction.memberPermissions : this.dmChannelPermissions; + + return this.sharedRun(required, permissions, 'context menu'); + } + + private sharedRun(requiredPermissions: Permissions, availablePermissions: Permissions | null, commandType: string) { + if (!availablePermissions) { return this.error({ - identifier: Identifiers.PreconditionClientPermissionsNoPermissions, - message: "I was unable to resolve the end-user's permissions in the command invocation channel." + identifier: Identifiers.PreconditionUserPermissionsNoPermissions, + message: `I was unable to resolve the end-user's permissions in the ${commandType} command invocation channel.` }); } - const missing = permissions.missing(required); + const missing = availablePermissions.missing(requiredPermissions); return missing.length === 0 ? this.ok() : this.error({ identifier: Identifiers.PreconditionUserPermissions, message: `You are missing the following permissions to run this command: ${missing - .map((perm) => ClientPermissionsPrecondition.readablePermissions[perm]) + .map((perm) => ClientPrecondition.readablePermissions[perm]) .join(', ')}`, context: { missing } }); diff --git a/src/preconditions/index.ts b/src/preconditions/index.ts index 5dfd71d7a..f51236f90 100644 --- a/src/preconditions/index.ts +++ b/src/preconditions/index.ts @@ -1,5 +1,5 @@ -export { CorePrecondition as ClientPermissions } from './ClientPermissions'; -export { CorePrecondition as Cooldown } from './Cooldown'; +export { CorePrecondition as ClientPermissions, UserPermissionsPreconditionContext } from './ClientPermissions'; +export { CorePrecondition as Cooldown, CooldownContext } from './Cooldown'; export { CorePrecondition as DMOnly } from './DMOnly'; export { CorePrecondition as Enabled } from './Enabled'; export { CorePrecondition as GuildNewsOnly } from './GuildNewsOnly'; diff --git a/tests/application-commands/computeDifferences.test.ts b/tests/application-commands/computeDifferences.test.ts new file mode 100644 index 000000000..1fff910c6 --- /dev/null +++ b/tests/application-commands/computeDifferences.test.ts @@ -0,0 +1,1341 @@ +import { ApplicationCommandOptionType, ApplicationCommandType, RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v9'; +import { getCommandDifferences } from '../../src/lib/utils/application-commands/computeDifferences'; + +describe('Compute differences for provided application commands', () => { + it('given two identical context menu commands, it should not return any difference', () => { + expect( + getCommandDifferences( + { + type: ApplicationCommandType.Message, + name: 'boop' + }, + { + type: ApplicationCommandType.Message, + name: 'boop' + } + ) + ).toEqual([]); + }); + + it('given one context menu command with one name and one context menu command with a different name, it should return the difference', () => { + expect( + getCommandDifferences( + { + type: ApplicationCommandType.Message, + name: 'boop' + }, + { + type: ApplicationCommandType.Message, + name: 'beep' + } + ) + ).toEqual([ + { + key: 'name', + original: 'boop', + expected: 'beep' + } + ]); + }); + + it('given a context menu command with a default_permission set to true and one set to false, it should return the difference', () => { + expect( + getCommandDifferences( + { + type: ApplicationCommandType.Message, + name: 'boop', + default_permission: true + }, + { + type: ApplicationCommandType.Message, + name: 'boop', + default_permission: false + } + ) + ).toEqual([ + { + key: 'defaultPermission', + original: 'true', + expected: 'false' + } + ]); + }); + + it('given two identical commands, it should not return any difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + expect(getCommandDifferences(command1, command2)).toEqual([]); + }); + + it('given 2 different descriptions, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 2', + name: 'command1' + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'description', + original: command1.description, + expected: command2.description + } + ]); + }); + + it('given a command with a default_permission set to true and one set to false, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + default_permission: true + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + default_permission: false + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'defaultPermission', + original: String(command1.default_permission), + expected: String(command2.default_permission) + } + ]); + }); + + it('given a command with no options and one with options, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options', + original: 'no options present', + expected: 'options present' + } + ]); + }); + + it('given a command with options and one without options, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options', + original: 'options present', + expected: 'no options present' + } + ]); + }); + + it('given a command with 1 option and one with 2 options, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option1' + }, + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option2' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[1]', + original: 'no option present', + expected: 'boolean option with name option2' + } + ]); + }); + + it('given a command with 1 string option and one with 1 boolean option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].type', + original: 'string option', + expected: 'boolean option' + } + ]); + }); + + it('given a command with 1 string option and one with 1 string option named differently, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option2' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].name', + original: 'option1', + expected: 'option2' + } + ]); + }); + + it('given a command with 1 string option and one with 1 string option with different descriptions, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 2', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].description', + original: 'description 1', + expected: 'description 2' + } + ]); + }); + + it('given a command with a required option and one with an option that is not required, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + required: true + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].required', + original: 'true', + expected: 'false' + } + ]); + }); + + it('given a command with 2 options and one with 1 option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + }, + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option2' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'existing command option at index 1', + expected: 'no option present', + original: 'string option with name option2' + } + ]); + }); + + it('given a command with 1 sub command group that has 1 sub command and one with 1 sub command group that has 2 sub commands, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand + }, + { + name: 'subcommand2', + description: 'description 2', + type: ApplicationCommandOptionType.Subcommand + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].options[1]', + original: 'no option present', + expected: 'subcommand with name subcommand2' + } + ]); + }); + + it('given a command with 1 sub command with no options and one command with 1 sub command with 1 option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].options[0].options', + original: 'no options present', + expected: 'options present' + } + ]); + }); + + it('given a command with 1 sub command with 1 option and one command with 1 sub command with no options, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].options[0].options', + expected: 'no options present', + original: 'options present' + } + ]); + }); + + it('given a command with 1 sub command with 2 options and one command with 1 sub command with 1 option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + }, + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option2' + } + ] + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'existing command option at path options[0].options[0].options[1]', + expected: 'no option present', + original: 'string option with name option2' + } + ]); + }); + + it('given a command with 1 sub command with 1 string option and one command with 1 sub command with 1 boolean option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + description: 'description 1', + name: 'option1', + required: true, + options: [ + { + name: 'subcommand1', + description: 'description 1', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.Boolean, + description: 'description 1', + name: 'option1' + } + ] + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].options[0].options[0].type', + expected: 'boolean option', + original: 'string option' + } + ]); + }); + + it('given a command with no options and one with an empty options array, it should not throw an error', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [] + }; + + expect(() => getCommandDifferences(command1, command2)).not.toThrow(); + expect(() => getCommandDifferences(command2, command1)).not.toThrow(); + }); + + it('given two commands with different names, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1' + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command2' + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'name', + expected: 'command2', + original: 'command1' + } + ]); + }); + + it('given a command with a number option and a command with a number option expecting a minimum value of 69, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + min_value: 69 + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].min_value', + expected: 'min_value present', + original: 'no min_value present' + } + ]); + }); + + it('given a command with a number option expecting a minimum value of 69 and a command with a number option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + min_value: 69 + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].min_value', + expected: 'no min_value present', + original: 'min_value present' + } + ]); + }); + + it('given a command with a number option expecting a minimum value of 69 and a command with a number option expecting a minimum value of 420, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + min_value: 69 + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + min_value: 420 + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].min_value', + expected: '420', + original: '69' + } + ]); + }); + + // + + it('given a command with a number option and a command with a number option expecting a maximum value of 69, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + max_value: 69 + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].max_value', + expected: 'max_value present', + original: 'no max_value present' + } + ]); + }); + + it('given a command with a number option expecting a maximum value of 69 and a command with a number option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + max_value: 69 + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].max_value', + expected: 'no max_value present', + original: 'max_value present' + } + ]); + }); + + it('given a command with a number option expecting a maximum value of 69 and a command with a number option expecting a maximum value of 420, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + max_value: 69 + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + max_value: 420 + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].max_value', + expected: '420', + original: '69' + } + ]); + }); + + it('given a command with a string option and a command with a string option that can be autocompleted, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + autocomplete: true + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].autocomplete', + expected: 'autocomplete enabled', + original: 'autocomplete disabled' + } + ]); + }); + + it('given a command with a string option that can be autocompleted and a command with a string option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + autocomplete: true + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].autocomplete', + expected: 'autocomplete disabled', + original: 'autocomplete enabled' + } + ]); + }); + + it('given a command with a string option and a command with a string option with a choice, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].choices', + expected: 'choices present', + original: 'no choices present' + } + ]); + }); + + it('given a command with a string option with a choice and a command with a string option, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1' + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].choices', + expected: 'no choices present', + original: 'choices present' + } + ]); + }); + + it('given a command with a string option with a choice and a command with a string option with two choices, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + }, + { + name: 'choice2', + value: 'value2' + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].choices[1]', + expected: 'no choice present', + original: 'choice present' + } + ]); + }); + + it('given a command with a string option with a choice and a command with a string option with a choice with a different name, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice2', + value: 'value1' + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].choices[0].name', + expected: 'choice2', + original: 'choice1' + } + ]); + }); + + it('given a command with a string option with a choice and a command with a string option with a choice with a different value, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value2' + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'options[0].choices[0].value', + expected: 'value2', + original: 'value1' + } + ]); + }); + + it('given a command with a string option with three choices and a command with a string option with one choice, it should return the difference', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + }, + { + name: 'choice2', + value: 'value2' + }, + { + name: 'choice3', + value: 'value3' + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.String, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 'value1' + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'existing choice at path options[0].choices[1]', + expected: 'no choice present', + original: 'choice with name "choice2" and value "value2" present' + }, + { + key: 'existing choice at path options[0].choices[2]', + expected: 'no choice present', + original: 'choice with name "choice3" and value "value3" present' + } + ]); + }); + + it('given a command with a number option with two choices and a command with a number option with one choice, it should return the differences', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 1 + }, + { + name: 'choice2', + value: 2 + } + ] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'command1', + options: [ + { + type: ApplicationCommandOptionType.Number, + description: 'description 1', + name: 'option1', + choices: [ + { + name: 'choice1', + value: 1 + } + ] + } + ] + }; + + expect(getCommandDifferences(command1, command2)).toEqual([ + { + key: 'existing choice at path options[0].choices[1]', + expected: 'no choice present', + original: 'choice with name "choice2" and value 2 present' + } + ]); + }); +}); diff --git a/yarn.lock b/yarn.lock index 32b96f3ef..c8ad78d75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,200 +5,200 @@ __metadata: version: 5 cacheKey: 8 -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/code-frame@npm:7.16.0" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/code-frame@npm:7.16.7" dependencies: - "@babel/highlight": ^7.16.0 - checksum: 8961d0302ec6b8c2e9751a11e06a17617425359fd1645e4dae56a90a03464c68a0916115100fbcd030961870313f21865d0b85858360a2c68aabdda744393607 + "@babel/highlight": ^7.16.7 + checksum: db2f7faa31bc2c9cf63197b481b30ea57147a5fc1a6fab60e5d6c02cdfbf6de8e17b5121f99917b3dabb5eeb572da078312e70697415940383efc140d4e0808b languageName: node linkType: hard -"@babel/compat-data@npm:^7.16.0": +"@babel/compat-data@npm:^7.16.4": version: 7.16.4 resolution: "@babel/compat-data@npm:7.16.4" checksum: 4949ce54eafc4b38d5623696a872acaaced1a523605708d81c2c483253941917d90dae0de40fc01e152ae56075dadd89c23014da5a632b09c001a716fa689cae languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5": - version: 7.16.5 - resolution: "@babel/core@npm:7.16.5" +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.2, @babel/core@npm:^7.8.0": + version: 7.16.7 + resolution: "@babel/core@npm:7.16.7" dependencies: - "@babel/code-frame": ^7.16.0 - "@babel/generator": ^7.16.5 - "@babel/helper-compilation-targets": ^7.16.3 - "@babel/helper-module-transforms": ^7.16.5 - "@babel/helpers": ^7.16.5 - "@babel/parser": ^7.16.5 - "@babel/template": ^7.16.0 - "@babel/traverse": ^7.16.5 - "@babel/types": ^7.16.0 + "@babel/code-frame": ^7.16.7 + "@babel/generator": ^7.16.7 + "@babel/helper-compilation-targets": ^7.16.7 + "@babel/helper-module-transforms": ^7.16.7 + "@babel/helpers": ^7.16.7 + "@babel/parser": ^7.16.7 + "@babel/template": ^7.16.7 + "@babel/traverse": ^7.16.7 + "@babel/types": ^7.16.7 convert-source-map: ^1.7.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.1.2 semver: ^6.3.0 source-map: ^0.5.0 - checksum: e5b76c6be95ab56a441772173463a56f824b39eba5fd3efe4b9784863922a1cb8abde6331d894854ed563b5ffe4be76d52524ecd07963660bb146f49a3cb3556 + checksum: 3206e077e76db189726c4da19a5296eae11c6c1f5abea7013e74f18708bb91616914717ff8d8ca466cc0ba9d2d2147e9a84c3c357b9ad4cba601da14107838ed languageName: node linkType: hard -"@babel/generator@npm:^7.16.5, @babel/generator@npm:^7.7.2": - version: 7.16.5 - resolution: "@babel/generator@npm:7.16.5" +"@babel/generator@npm:^7.16.7, @babel/generator@npm:^7.7.2": + version: 7.16.7 + resolution: "@babel/generator@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 + "@babel/types": ^7.16.7 jsesc: ^2.5.1 source-map: ^0.5.0 - checksum: 621fa2da21a5397a4739f03af1eda76140f0da9f962071640a479c0cf1859edc576aa8881b5771be9274238f048bf9024c94d826003659f64eee29c48f2fe470 + checksum: 20c6a7c5e372a66ec2900c074b2ec3634d3f615cafccbb416770f4b419251c6dc27a0a137b71407e218463fe059a3a6a5afb734f35089d94bdb66e01fe8a9e6f languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.16.3": - version: 7.16.3 - resolution: "@babel/helper-compilation-targets@npm:7.16.3" +"@babel/helper-compilation-targets@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-compilation-targets@npm:7.16.7" dependencies: - "@babel/compat-data": ^7.16.0 - "@babel/helper-validator-option": ^7.14.5 + "@babel/compat-data": ^7.16.4 + "@babel/helper-validator-option": ^7.16.7 browserslist: ^4.17.5 semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: 038bcd43ac914371c51bf6e72b5cedcae432f0d359285d74a9133c6a839bd625a7d5412d7471d50aa78a3e1c79b0a692b50a8d6a1299ebf69733b512ff199323 + checksum: 7238aaee78c011a42fb5ca92e5eff098752f7b314c2111d7bb9cdd58792fcab1b9c819b59f6a0851dc210dc09dc06b30d130a23982753e70eb3111bc65204842 languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.16.5": - version: 7.16.5 - resolution: "@babel/helper-environment-visitor@npm:7.16.5" +"@babel/helper-environment-visitor@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-environment-visitor@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 - checksum: f57da613f2fb9ca0b85cb4a9131cb688555e78ba8b0047ac0e73551b247eb71bf8fa075e6408064e8ab71ec230f24b4e06367efc9ccd1dcfcea0efe0086f02f3 + "@babel/types": ^7.16.7 + checksum: c03a10105d9ebd1fe632a77356b2e6e2f3c44edba9a93b0dc3591b6a66bd7a2e323dd9502f9ce96fc6401234abff1907aa877b6674f7826b61c953f7c8204bbe languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/helper-function-name@npm:7.16.0" +"@babel/helper-function-name@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-function-name@npm:7.16.7" dependencies: - "@babel/helper-get-function-arity": ^7.16.0 - "@babel/template": ^7.16.0 - "@babel/types": ^7.16.0 - checksum: 8c02371d28678f3bb492e69d4635b2fe6b1c5a93ce129bf883f1fafde2005f4dbc0e643f52103ca558b698c0774bfb84a93f188d71db1c077f754b6220629b92 + "@babel/helper-get-function-arity": ^7.16.7 + "@babel/template": ^7.16.7 + "@babel/types": ^7.16.7 + checksum: fc77cbe7b10cfa2a262d7a37dca575c037f20419dfe0c5d9317f589599ca24beb5f5c1057748011159149eaec47fe32338c6c6412376fcded68200df470161e1 languageName: node linkType: hard -"@babel/helper-get-function-arity@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/helper-get-function-arity@npm:7.16.0" +"@babel/helper-get-function-arity@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-get-function-arity@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 - checksum: 1a68322c7b5fdffb1b51df32f7a53b1ff2268b5b99d698f0a1a426dcb355482a44ef3dae982a507907ba975314638dabb6d77ac1778098bdbe99707e6c29cae8 + "@babel/types": ^7.16.7 + checksum: 25d969fb207ff2ad5f57a90d118f6c42d56a0171022e200aaa919ba7dc95ae7f92ec71cdea6c63ef3629a0dc962ab4c78e09ca2b437185ab44539193f796e0c3 languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/helper-hoist-variables@npm:7.16.0" +"@babel/helper-hoist-variables@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-hoist-variables@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 - checksum: 2ee5b400c267c209a53c90eea406a8f09c30d4d7a2b13e304289d858a2e34a99272c062cfad6dad63705662943951c42ff20042ef539b2d3c4f8743183a28954 + "@babel/types": ^7.16.7 + checksum: 6ae1641f4a751cd9045346e3f61c3d9ec1312fd779ab6d6fecfe2a96e59a481ad5d7e40d2a840894c13b3fd6114345b157f9e3062fc5f1580f284636e722de60 languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/helper-module-imports@npm:7.16.0" +"@babel/helper-module-imports@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-module-imports@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 - checksum: 8e1eb9ac39440e52080b87c78d8d318e7c93658bdd0f3ce0019c908de88cbddafdc241f392898c0b0ba81fc52c8c6d2f9cc1b163ac5ed2a474d49b11646b7516 + "@babel/types": ^7.16.7 + checksum: ddd2c4a600a2e9a4fee192ab92bf35a627c5461dbab4af31b903d9ba4d6b6e59e0ff3499fde4e2e9a0eebe24906f00b636f8b4d9bd72ff24d50e6618215c3212 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.16.5": - version: 7.16.5 - resolution: "@babel/helper-module-transforms@npm:7.16.5" +"@babel/helper-module-transforms@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-module-transforms@npm:7.16.7" dependencies: - "@babel/helper-environment-visitor": ^7.16.5 - "@babel/helper-module-imports": ^7.16.0 - "@babel/helper-simple-access": ^7.16.0 - "@babel/helper-split-export-declaration": ^7.16.0 - "@babel/helper-validator-identifier": ^7.15.7 - "@babel/template": ^7.16.0 - "@babel/traverse": ^7.16.5 - "@babel/types": ^7.16.0 - checksum: 0463e7198e5540cbb90981f769c89ec302001b211c33df1a6790a1eaee678ec418cee40ef3cf0fe159d40787214fbba129582f6b07e79244dc8cbcd5e791dd18 + "@babel/helper-environment-visitor": ^7.16.7 + "@babel/helper-module-imports": ^7.16.7 + "@babel/helper-simple-access": ^7.16.7 + "@babel/helper-split-export-declaration": ^7.16.7 + "@babel/helper-validator-identifier": ^7.16.7 + "@babel/template": ^7.16.7 + "@babel/traverse": ^7.16.7 + "@babel/types": ^7.16.7 + checksum: 6e930ce776c979f299cdbeaf80187f4ab086d75287b96ecc1c6896d392fcb561065f0d6219fc06fa79b4ceb4bbdc1a9847da8099aba9b077d0a9e583500fb673 languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.5, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.16.5 - resolution: "@babel/helper-plugin-utils@npm:7.16.5" - checksum: 3ff605f879a9ed287952b538a8334bb16e6cf7cf441f205713b1cf8043b047a965773b66e50575018504f349e16368acfe4702a2f376e16263733e2c7c6c3e39 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.16.7 + resolution: "@babel/helper-plugin-utils@npm:7.16.7" + checksum: d08dd86554a186c2538547cd537552e4029f704994a9201d41d82015c10ed7f58f9036e8d1527c3760f042409163269d308b0b3706589039c5f1884619c6d4ce languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/helper-simple-access@npm:7.16.0" +"@babel/helper-simple-access@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-simple-access@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 - checksum: 2d7155f318411788b42d2f4a3d406de12952ad620d0bd411a0f3b5803389692ad61d9e7fab5f93b23ad3d8a09db4a75ca9722b9873a606470f468bc301944af6 + "@babel/types": ^7.16.7 + checksum: 8d22c46c5ec2ead0686c4d5a3d1d12b5190c59be676bfe0d9d89df62b437b51d1a3df2ccfb8a77dded2e585176ebf12986accb6d45a18cff229eef3b10344f4b languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/helper-split-export-declaration@npm:7.16.0" +"@babel/helper-split-export-declaration@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-split-export-declaration@npm:7.16.7" dependencies: - "@babel/types": ^7.16.0 - checksum: 8bd87b5ea2046b145f0f55bc75cbdb6df69eaeb32919ee3c1c758757025aebca03e567a4d48389eb4f16a55021adb6ed8fa58aa771e164b15fa5e0a0722f771d + "@babel/types": ^7.16.7 + checksum: e10aaf135465c55114627951b79115f24bc7af72ecbb58d541d66daf1edaee5dde7cae3ec8c3639afaf74526c03ae3ce723444e3b5b3dc77140c456cd84bcaa1 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.15.7": - version: 7.15.7 - resolution: "@babel/helper-validator-identifier@npm:7.15.7" - checksum: f041c28c531d1add5cc345b25d5df3c29c62bce3205b4d4a93dcd164ccf630350acba252d374fad8f5d8ea526995a215829f27183ba7ce7ce141843bf23068a6 +"@babel/helper-validator-identifier@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-validator-identifier@npm:7.16.7" + checksum: dbb3db9d184343152520a209b5684f5e0ed416109cde82b428ca9c759c29b10c7450657785a8b5c5256aa74acc6da491c1f0cf6b784939f7931ef82982051b69 languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.14.5": - version: 7.14.5 - resolution: "@babel/helper-validator-option@npm:7.14.5" - checksum: 1b25c34a5cb3d8602280f33b9ab687d2a77895e3616458d0f70ddc450ada9b05e342c44f322bc741d51b252e84cff6ec44ae93d622a3354828579a643556b523 +"@babel/helper-validator-option@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helper-validator-option@npm:7.16.7" + checksum: c5ccc451911883cc9f12125d47be69434f28094475c1b9d2ada7c3452e6ac98a1ee8ddd364ca9e3f9855fcdee96cdeafa32543ebd9d17fee7a1062c202e80570 languageName: node linkType: hard -"@babel/helpers@npm:^7.16.5": - version: 7.16.5 - resolution: "@babel/helpers@npm:7.16.5" +"@babel/helpers@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/helpers@npm:7.16.7" dependencies: - "@babel/template": ^7.16.0 - "@babel/traverse": ^7.16.5 - "@babel/types": ^7.16.0 - checksum: 960d938a4359b7f9ff7b753e33b6f600e269aec0ef6030c8026ac37525103da8cde5f1c04ce7de1ad6fc37707aa6178eae938d6fc82544aa25c9fd602c62e0a8 + "@babel/template": ^7.16.7 + "@babel/traverse": ^7.16.7 + "@babel/types": ^7.16.7 + checksum: 75504c76b66a29b91f954fcc0867dfe275a4cfba5b44df6d64405df74ea72f967fccfa63d62c31c423c5502d113290000c581e0e4858a214f0303d7ecf55c29f languageName: node linkType: hard -"@babel/highlight@npm:^7.16.0": - version: 7.16.0 - resolution: "@babel/highlight@npm:7.16.0" +"@babel/highlight@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/highlight@npm:7.16.7" dependencies: - "@babel/helper-validator-identifier": ^7.15.7 + "@babel/helper-validator-identifier": ^7.16.7 chalk: ^2.0.0 js-tokens: ^4.0.0 - checksum: abf244c48fcff20ec87830e8b99c776f4dcdd9138e63decc195719a94148da35339639e0d8045eb9d1f3e67a39ab90a9c3f5ce2d579fb1a0368d911ddf29b4e5 + checksum: f7e04e7e03b83c2cca984f4d3e180c9b018784f45d03367e94daf983861229ddc47264045f3b58dfeb0007f9c67bc2a76c4de1693bad90e5394876ef55ece5bb languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.0, @babel/parser@npm:^7.16.5, @babel/parser@npm:^7.7.2": - version: 7.16.6 - resolution: "@babel/parser@npm:7.16.6" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.7": + version: 7.16.7 + resolution: "@babel/parser@npm:7.16.7" bin: parser: ./bin/babel-parser.js - checksum: 5cbb01a7b2ba5d609945099bfadb01f54e11ef85201e1e0bf47010ee1b35c257eca6ff91606c6ce8adba82a95e180b583183e4dc076f4a70e706152075dd98ca + checksum: e664ff1edda164ab3f3c97fc1dd1a8930b0fba9981cbf873d3f25a22d16d50e2efcfaf81daeefa978bff2c4f268d34832f6817c8bc4e03594c3f43beba92fb68 languageName: node linkType: hard @@ -335,52 +335,52 @@ __metadata: linkType: hard "@babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.16.5 - resolution: "@babel/plugin-syntax-typescript@npm:7.16.5" + version: 7.16.7 + resolution: "@babel/plugin-syntax-typescript@npm:7.16.7" dependencies: - "@babel/helper-plugin-utils": ^7.16.5 + "@babel/helper-plugin-utils": ^7.16.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 73454e8e9d5be92304d60d457203b43e04a9d331c4234eefad390a3a4d36a30d75b211ba9e98205e0b322a6c178e46b5852da35889eef9183549d6589d04a01e + checksum: 661e636060609ede9a402e22603b01784c21fabb0a637e65f561c8159351fe0130bbc11fdefe31902107885e3332fc34d95eb652ac61d3f61f2d61f5da20609e languageName: node linkType: hard -"@babel/template@npm:^7.16.0, @babel/template@npm:^7.3.3": - version: 7.16.0 - resolution: "@babel/template@npm:7.16.0" +"@babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3": + version: 7.16.7 + resolution: "@babel/template@npm:7.16.7" dependencies: - "@babel/code-frame": ^7.16.0 - "@babel/parser": ^7.16.0 - "@babel/types": ^7.16.0 - checksum: 940f105cc6a6aee638cd8cfae80b8b80811e0ddd53b6a11f3a68431ebb998564815fb26511b5d9cb4cff66ea67130ba7498555ee015375d32f5f89ceaa6662ea + "@babel/code-frame": ^7.16.7 + "@babel/parser": ^7.16.7 + "@babel/types": ^7.16.7 + checksum: 10cd112e89276e00f8b11b55a51c8b2f1262c318283a980f4d6cdb0286dc05734b9aaeeb9f3ad3311900b09bc913e02343fcaa9d4a4f413964aaab04eb84ac4a languageName: node linkType: hard -"@babel/traverse@npm:^7.16.5, @babel/traverse@npm:^7.7.2": - version: 7.16.5 - resolution: "@babel/traverse@npm:7.16.5" +"@babel/traverse@npm:^7.16.7, @babel/traverse@npm:^7.7.2": + version: 7.16.7 + resolution: "@babel/traverse@npm:7.16.7" dependencies: - "@babel/code-frame": ^7.16.0 - "@babel/generator": ^7.16.5 - "@babel/helper-environment-visitor": ^7.16.5 - "@babel/helper-function-name": ^7.16.0 - "@babel/helper-hoist-variables": ^7.16.0 - "@babel/helper-split-export-declaration": ^7.16.0 - "@babel/parser": ^7.16.5 - "@babel/types": ^7.16.0 + "@babel/code-frame": ^7.16.7 + "@babel/generator": ^7.16.7 + "@babel/helper-environment-visitor": ^7.16.7 + "@babel/helper-function-name": ^7.16.7 + "@babel/helper-hoist-variables": ^7.16.7 + "@babel/helper-split-export-declaration": ^7.16.7 + "@babel/parser": ^7.16.7 + "@babel/types": ^7.16.7 debug: ^4.1.0 globals: ^11.1.0 - checksum: 6bc31311b641ac0a1c6c854cad3faa172f54d987f9a28d7d75ed64ecbcc74983f60acd51bdd792f77e451fd5385c10ce9955f9d1d60162bd32748cc42dc7eef9 + checksum: 65261f7a5bf257c10a9415b6c227fb555ace359ad786645d9cf22f0e3fc8dc8e38895269f3b93cc39eccd8ed992e7bacc358b4cb7d3496fe54f91cda49220834 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": - version: 7.16.0 - resolution: "@babel/types@npm:7.16.0" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.7, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": + version: 7.16.7 + resolution: "@babel/types@npm:7.16.7" dependencies: - "@babel/helper-validator-identifier": ^7.15.7 + "@babel/helper-validator-identifier": ^7.16.7 to-fast-properties: ^2.0.0 - checksum: 5b483da5c6e6f2394fba7ee1da8787a0c9cddd33491271c4da702e49e6faf95ce41d7c8bf9a4ee47f2ef06bdb35096f4d0f6ae4b5bea35ebefe16309d22344b7 + checksum: df9210723259df9faea8c7e5674a59e57ead82664aab9f54daae887db5a50a956f30f57ed77a2d6cbb89b908d520cf8d883267c4e9098e31bc74649f2f714654 languageName: node linkType: hard @@ -657,20 +657,21 @@ __metadata: languageName: node linkType: hard -"@favware/rollup-type-bundler@npm:^1.0.6": - version: 1.0.6 - resolution: "@favware/rollup-type-bundler@npm:1.0.6" +"@favware/rollup-type-bundler@npm:^1.0.7": + version: 1.0.7 + resolution: "@favware/rollup-type-bundler@npm:1.0.7" dependencies: - "@sapphire/utilities": ^3.0.3 + "@sapphire/utilities": ^3.1.0 colorette: ^2.0.16 - commander: ^8.2.0 + commander: ^8.3.0 js-yaml: ^4.1.0 - rollup: ^2.58.0 - rollup-plugin-dts: ^4.0.0 + rollup: ^2.63.0 + rollup-plugin-dts: ^4.1.0 + typescript: ^4.5.4 bin: rollup-type-bundler: dist/cli.js rtb: dist/cli.js - checksum: dd25718735ecbcb86fc8e194ca94cad92deca4032a8b7aa90a068825f675d31cd667fc73805422c9d20070af917ab7d14b1fee93adff1e4e17ba06543608801c + checksum: e42a8c5b4509b78219b477acdd36baedfb97e48265796e3418baba085d0f9e65ce7ccab59c011a4eb320bcf778dfda8ff2a257910044846b02c2ca00baee73ec languageName: node linkType: hard @@ -726,28 +727,28 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:^27.4.2": - version: 27.4.2 - resolution: "@jest/console@npm:27.4.2" +"@jest/console@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/console@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 "@types/node": "*" chalk: ^4.0.0 - jest-message-util: ^27.4.2 + jest-message-util: ^27.4.6 jest-util: ^27.4.2 slash: ^3.0.0 - checksum: d285de0ad924a726c0a1b472968e749a88e33fc5b5af4ef06c1eea5f9f489701ebd81da1b70837fcb810e8d66f8e925d6e49be2cd5a3842304d00b54a81ff14f + checksum: 603408498d2fd7fa6cfb85cc18a5823747c824be2f88be526ed4db83df65db7a9d3a93056eeaddd32ea1517d581b94862e532ccde081e0ecf9d82ac743ec6ac2 languageName: node linkType: hard -"@jest/core@npm:^27.4.5": - version: 27.4.5 - resolution: "@jest/core@npm:27.4.5" +"@jest/core@npm:^27.4.7": + version: 27.4.7 + resolution: "@jest/core@npm:27.4.7" dependencies: - "@jest/console": ^27.4.2 - "@jest/reporters": ^27.4.5 - "@jest/test-result": ^27.4.2 - "@jest/transform": ^27.4.5 + "@jest/console": ^27.4.6 + "@jest/reporters": ^27.4.6 + "@jest/test-result": ^27.4.6 + "@jest/transform": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" ansi-escapes: ^4.2.1 @@ -756,18 +757,18 @@ __metadata: exit: ^0.1.2 graceful-fs: ^4.2.4 jest-changed-files: ^27.4.2 - jest-config: ^27.4.5 - jest-haste-map: ^27.4.5 - jest-message-util: ^27.4.2 + jest-config: ^27.4.7 + jest-haste-map: ^27.4.6 + jest-message-util: ^27.4.6 jest-regex-util: ^27.4.0 - jest-resolve: ^27.4.5 - jest-resolve-dependencies: ^27.4.5 - jest-runner: ^27.4.5 - jest-runtime: ^27.4.5 - jest-snapshot: ^27.4.5 + jest-resolve: ^27.4.6 + jest-resolve-dependencies: ^27.4.6 + jest-runner: ^27.4.6 + jest-runtime: ^27.4.6 + jest-snapshot: ^27.4.6 jest-util: ^27.4.2 - jest-validate: ^27.4.2 - jest-watcher: ^27.4.2 + jest-validate: ^27.4.6 + jest-watcher: ^27.4.6 micromatch: ^4.0.4 rimraf: ^3.0.0 slash: ^3.0.0 @@ -777,55 +778,55 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: d9332952196018abfc0b5cbbc9062f71872859bbe7a55b98788fc7b2f30fec1286d2dd882d8aa75fa14f5aeea8401a3eaacfed88dc86b159934dc35e06a2cadd + checksum: 24ed123ef1819fa8c6069706760efac9904ee8824b22c346259be2017d820b5e578a4d444339448a576a0158e6fec91d18fdedb201bc97d7390b105d665f3642 languageName: node linkType: hard -"@jest/environment@npm:^27.4.4": - version: 27.4.4 - resolution: "@jest/environment@npm:27.4.4" +"@jest/environment@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/environment@npm:27.4.6" dependencies: - "@jest/fake-timers": ^27.4.2 + "@jest/fake-timers": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" - jest-mock: ^27.4.2 - checksum: 59296abb5d073b7a5f24faba6d39e716cbbba077b7477e944a46cfdc7a0624035e4c78c3cb8d27e0875ecb26a1526720be177a9e1aef0efed8e7ba8dd9fb4b6e + jest-mock: ^27.4.6 + checksum: c3aadcf6d42e55e35d8020f7cf5054c445775608e466fcfc37348359e54f2f79e0e39d029281836ae9082dc50eac81d1cf6b4fc3899adfb58afc68a7c72f8e3d languageName: node linkType: hard -"@jest/fake-timers@npm:^27.4.2": - version: 27.4.2 - resolution: "@jest/fake-timers@npm:27.4.2" +"@jest/fake-timers@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/fake-timers@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 "@sinonjs/fake-timers": ^8.0.1 "@types/node": "*" - jest-message-util: ^27.4.2 - jest-mock: ^27.4.2 + jest-message-util: ^27.4.6 + jest-mock: ^27.4.6 jest-util: ^27.4.2 - checksum: 4b0c21ce8aec687ccd4e96b6f9d532a9848517b5e5fc8fa96a90fe1e7514952d0e1f805e6539fbd7336fbbac05e1a4ec7915c59284c40d919fcfb1a226b3bc9d + checksum: 389f655d39f13fdd0448b554260cd41810cf824b99e9de057600869a708d34cfa74e7fdaba5fcd6e3295e7bfed08f1b3fc0735ca86f7c0b2281b25e534032876 languageName: node linkType: hard -"@jest/globals@npm:^27.4.4": - version: 27.4.4 - resolution: "@jest/globals@npm:27.4.4" +"@jest/globals@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/globals@npm:27.4.6" dependencies: - "@jest/environment": ^27.4.4 + "@jest/environment": ^27.4.6 "@jest/types": ^27.4.2 - expect: ^27.4.2 - checksum: b43d8290fbd09148961877cc859c4e23e4c7cb44c161d540fd7ab8f9dc490cf787dc346c308d7df9d23429461754156b78b36bc14b78823f51c3869106e2e0c6 + expect: ^27.4.6 + checksum: a438645771f45557b3af6e371e65c88e109d7433d3d4ee5db908177f29be6d6d12b4cfe9279ae6475bc033b5ff2a97235659a75f2718855041dd3ed805ed2edd languageName: node linkType: hard -"@jest/reporters@npm:^27.4.5": - version: 27.4.5 - resolution: "@jest/reporters@npm:27.4.5" +"@jest/reporters@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/reporters@npm:27.4.6" dependencies: "@bcoe/v8-coverage": ^0.2.3 - "@jest/console": ^27.4.2 - "@jest/test-result": ^27.4.2 - "@jest/transform": ^27.4.5 + "@jest/console": ^27.4.6 + "@jest/test-result": ^27.4.6 + "@jest/transform": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" chalk: ^4.0.0 @@ -834,14 +835,14 @@ __metadata: glob: ^7.1.2 graceful-fs: ^4.2.4 istanbul-lib-coverage: ^3.0.0 - istanbul-lib-instrument: ^4.0.3 + istanbul-lib-instrument: ^5.1.0 istanbul-lib-report: ^3.0.0 istanbul-lib-source-maps: ^4.0.0 - istanbul-reports: ^3.0.2 - jest-haste-map: ^27.4.5 - jest-resolve: ^27.4.5 + istanbul-reports: ^3.1.3 + jest-haste-map: ^27.4.6 + jest-resolve: ^27.4.6 jest-util: ^27.4.2 - jest-worker: ^27.4.5 + jest-worker: ^27.4.6 slash: ^3.0.0 source-map: ^0.6.0 string-length: ^4.0.1 @@ -852,7 +853,7 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: d053edae6906171f29c50c6129a600dd10d00320adf6df57938efc651ddd98aecdf7e3f82c3778e77311e8358e57e337d21c391aa867c9c289366e7bd4d6cf2b + checksum: 4c14b2cf6c9b624977f9ad519e9ce2f5ead4a3c9a3fa0b9c68097b7bc78b598ceb5402566417d81e16489dbd6bb6e97e58f04c22099013897dd6010c0549b169 languageName: node linkType: hard @@ -867,50 +868,50 @@ __metadata: languageName: node linkType: hard -"@jest/test-result@npm:^27.4.2": - version: 27.4.2 - resolution: "@jest/test-result@npm:27.4.2" +"@jest/test-result@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/test-result@npm:27.4.6" dependencies: - "@jest/console": ^27.4.2 + "@jest/console": ^27.4.6 "@jest/types": ^27.4.2 "@types/istanbul-lib-coverage": ^2.0.0 collect-v8-coverage: ^1.0.0 - checksum: bc3b91a76b505c7367e15d318ce49332e56857b9f6a00f67e9debfcbd11f22f90942b3e0aeea44b7e8da1fecba4fcb6ac591d007e488c300e361b763cf3b65b9 + checksum: ddfc5783f2025ba979df395ddead7f76aac91df9a8a4ab15d5b1210a58e523932bb9ea9e1e97229c09cab81fdb2611292fdc8e56e2c5b44ed452ac11db7f79f0 languageName: node linkType: hard -"@jest/test-sequencer@npm:^27.4.5": - version: 27.4.5 - resolution: "@jest/test-sequencer@npm:27.4.5" +"@jest/test-sequencer@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/test-sequencer@npm:27.4.6" dependencies: - "@jest/test-result": ^27.4.2 + "@jest/test-result": ^27.4.6 graceful-fs: ^4.2.4 - jest-haste-map: ^27.4.5 - jest-runtime: ^27.4.5 - checksum: b78376fe4b964f2fd7e71083c220e5f0a8f59f079dc88783c60fce969b09ea38eebabc32c50a4637c20679a8bfa8220abb814cd232d241ee385d4df3d93f7d21 + jest-haste-map: ^27.4.6 + jest-runtime: ^27.4.6 + checksum: 8d761fd81f5cf4845a09844a8a16717fc148137f364916165ce5e1ebfc5dfd89160d4b98e7e947c97f8707500050863606d0becb8c388997efcc31cafa6f5e31 languageName: node linkType: hard -"@jest/transform@npm:^27.4.5": - version: 27.4.5 - resolution: "@jest/transform@npm:27.4.5" +"@jest/transform@npm:^27.4.6": + version: 27.4.6 + resolution: "@jest/transform@npm:27.4.6" dependencies: "@babel/core": ^7.1.0 "@jest/types": ^27.4.2 - babel-plugin-istanbul: ^6.0.0 + babel-plugin-istanbul: ^6.1.1 chalk: ^4.0.0 convert-source-map: ^1.4.0 fast-json-stable-stringify: ^2.0.0 graceful-fs: ^4.2.4 - jest-haste-map: ^27.4.5 + jest-haste-map: ^27.4.6 jest-regex-util: ^27.4.0 jest-util: ^27.4.2 micromatch: ^4.0.4 - pirates: ^4.0.1 + pirates: ^4.0.4 slash: ^3.0.0 source-map: ^0.6.1 write-file-atomic: ^3.0.0 - checksum: f7a479545969d327a253ff1963c20260cffdee50cbc1345205f06e206df09871dd3f62dd4ba5358a087587ef5fa320b2e32efe1166192d8da835065e99d6bce7 + checksum: b2500fc5a7e7cad34547acdb8930797f021cda6b811ed0626564999bfd9ca856f52cc3a9b2ced5d037f3bd06a49b8b30cb7c10259318dc67bd11a564854d2ca6 languageName: node linkType: hard @@ -1033,7 +1034,7 @@ __metadata: "@commitlint/cli": ^16.0.1 "@commitlint/config-conventional": ^16.0.0 "@favware/npm-deprecate": ^1.0.4 - "@favware/rollup-type-bundler": ^1.0.6 + "@favware/rollup-type-bundler": ^1.0.7 "@sapphire/discord-utilities": ^2.4.0 "@sapphire/discord.js-utilities": ^4.1.5 "@sapphire/eslint-config": ^4.0.8 @@ -1043,24 +1044,24 @@ __metadata: "@sapphire/ts-config": ^3.1.6 "@sapphire/utilities": ^3.1.0 "@types/jest": ^27.4.0 - "@types/node": ^17.0.4 + "@types/node": ^17.0.8 "@types/ws": ^8.2.2 - "@typescript-eslint/eslint-plugin": ^5.8.1 - "@typescript-eslint/parser": ^5.8.1 + "@typescript-eslint/eslint-plugin": ^5.9.0 + "@typescript-eslint/parser": ^5.9.0 cz-conventional-changelog: ^3.3.0 - discord.js: ^13.5.0 + discord.js: ^13.5.1 eslint: ^8.6.0 eslint-config-prettier: ^8.3.0 eslint-plugin-prettier: ^4.0.0 gen-esm-wrapper: ^1.1.3 husky: ^7.0.4 - jest: ^27.4.5 - jest-circus: ^27.4.5 + jest: ^27.4.7 + jest-circus: ^27.4.6 lexure: ^0.17.0 - lint-staged: ^12.1.4 + lint-staged: ^12.1.7 prettier: ^2.5.1 pretty-quick: ^3.1.3 - rollup: ^2.62.0 + rollup: ^2.63.0 rollup-plugin-version-injector: ^1.3.3 standard-version: ^9.3.2 ts-jest: ^27.1.2 @@ -1121,7 +1122,7 @@ __metadata: languageName: node linkType: hard -"@sapphire/utilities@npm:^3.0.1, @sapphire/utilities@npm:^3.0.3, @sapphire/utilities@npm:^3.1.0": +"@sapphire/utilities@npm:^3.0.1, @sapphire/utilities@npm:^3.1.0": version: 3.1.0 resolution: "@sapphire/utilities@npm:3.1.0" checksum: aa0628352faf8c52774740c53a26ca7154b133ffbe86efaf9684de67c9d8d913732f99108d15babf4e4f9c2bc6e71070466bc8585be9a7add1659533aa2b5716 @@ -1129,9 +1130,9 @@ __metadata: linkType: hard "@sindresorhus/is@npm:^4.2.0": - version: 4.2.0 - resolution: "@sindresorhus/is@npm:4.2.0" - checksum: 59040dfb75c2eb6ab76e8c7ac10b7f7f6ba740f0b5ac618a89a8bcdbaf923836a8e998078d59d81f6f13f4b6bbe15bfe1bca962c877edcbe9160d1c100c56fd7 + version: 4.2.1 + resolution: "@sindresorhus/is@npm:4.2.1" + checksum: 05405d6796bcc37e6298995cdbee8ce42dd05c4cc0268eed09156f1e289336d1360e4a3e059116f47e6c2354f14b59737fa804e01b196d59922e37c73a942b95 languageName: node linkType: hard @@ -1189,15 +1190,15 @@ __metadata: linkType: hard "@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14": - version: 7.1.17 - resolution: "@types/babel__core@npm:7.1.17" + version: 7.1.18 + resolution: "@types/babel__core@npm:7.1.18" dependencies: "@babel/parser": ^7.1.0 "@babel/types": ^7.0.0 "@types/babel__generator": "*" "@types/babel__template": "*" "@types/babel__traverse": "*" - checksum: 0108efab8acb6a8e0aab6f8113d5ef1fc4b58d40737aa70a3ee83112959e0880e5548374e7edb562e4e837cde4ae47265348b04eb7e684283b0dea418d013420 + checksum: 2e5b5d7c84f347d3789575486e58b0df5c91613abc3d27e716274aba3048518e07e1f068250ba829e2ed58532ccc88da595ce95ba2688e7bbcd7c25a3c6627ed languageName: node linkType: hard @@ -1304,10 +1305,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^17.0.4": - version: 17.0.4 - resolution: "@types/node@npm:17.0.4" - checksum: 92e6a25fea2314cd34e81962bd07c8b79b92cae04d84a0336a8c49a2b8aa4c34ff8cb428baeac2022daf597809bd3b7987c624b07a91c4d01b6230f82c293190 +"@types/node@npm:*, @types/node@npm:^17.0.8": + version: 17.0.8 + resolution: "@types/node@npm:17.0.8" + checksum: f4cadeb9e602027520abc88c77142697e33cf6ac98bb02f8b595a398603cbd33df1f94d01c055c9f13cde0c8eaafc5e396ca72645458d42b4318b845bc7f1d0f languageName: node linkType: hard @@ -1364,34 +1365,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.6.0": - version: 5.8.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.8.0" - dependencies: - "@typescript-eslint/experimental-utils": 5.8.0 - "@typescript-eslint/scope-manager": 5.8.0 - debug: ^4.3.2 - functional-red-black-tree: ^1.0.1 - ignore: ^5.1.8 - regexpp: ^3.2.0 - semver: ^7.3.5 - tsutils: ^3.21.0 - peerDependencies: - "@typescript-eslint/parser": ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 96a21a3e19baf57e30c97953e35832b1f4e135c865b2dfd5afe53772bd08556b9ad724e55696dce9acf471553ab66ae45737e82abba6c15152f79a47d2d9f055 - languageName: node - linkType: hard - -"@typescript-eslint/eslint-plugin@npm:^5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/eslint-plugin@npm:5.8.1" +"@typescript-eslint/eslint-plugin@npm:^5.6.0, @typescript-eslint/eslint-plugin@npm:^5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.9.0" dependencies: - "@typescript-eslint/experimental-utils": 5.8.1 - "@typescript-eslint/scope-manager": 5.8.1 + "@typescript-eslint/experimental-utils": 5.9.0 + "@typescript-eslint/scope-manager": 5.9.0 + "@typescript-eslint/type-utils": 5.9.0 debug: ^4.3.2 functional-red-black-tree: ^1.0.1 ignore: ^5.1.8 @@ -1404,134 +1384,82 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 9e5b5c1e22563fc0a31f1b916cea8b059b6dd218ccbf809b7453e4563065781e4544a6d5ce4cbf60b40394f2604e925d10cafd468a4dd0f490e75775267839a0 + checksum: 31443d4331dddf7618d6b3fdbf148ec6d5ce7c64c85ec3973e520e633467d8d5605896f7eab9d7c6f81c050458c84bca10a6b0ed3537d48e6ee728f8b64d46a2 languageName: node linkType: hard -"@typescript-eslint/experimental-utils@npm:5.8.0": - version: 5.8.0 - resolution: "@typescript-eslint/experimental-utils@npm:5.8.0" +"@typescript-eslint/experimental-utils@npm:5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/experimental-utils@npm:5.9.0" dependencies: "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.8.0 - "@typescript-eslint/types": 5.8.0 - "@typescript-eslint/typescript-estree": 5.8.0 + "@typescript-eslint/scope-manager": 5.9.0 + "@typescript-eslint/types": 5.9.0 + "@typescript-eslint/typescript-estree": 5.9.0 eslint-scope: ^5.1.1 eslint-utils: ^3.0.0 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: c97798bcc3332331a75661e073d38783ee4882803b0247db76df851bc8594c9b7e23fb9de28aa212c331b18ff2e8c23657ae1b9b994eeec528214fcf8d81e9fb - languageName: node - linkType: hard - -"@typescript-eslint/experimental-utils@npm:5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/experimental-utils@npm:5.8.1" - dependencies: - "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.8.1 - "@typescript-eslint/types": 5.8.1 - "@typescript-eslint/typescript-estree": 5.8.1 - eslint-scope: ^5.1.1 - eslint-utils: ^3.0.0 - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 15c17a7b7a45a9e1ebf537e6d6221e423c8f5114c0a517265698745b9a4ae965487ef7856a0b1ee64cbda8db641a9204270fda88398ab1d7013256e0ccbd3e75 - languageName: node - linkType: hard - -"@typescript-eslint/parser@npm:^5.6.0": - version: 5.8.0 - resolution: "@typescript-eslint/parser@npm:5.8.0" - dependencies: - "@typescript-eslint/scope-manager": 5.8.0 - "@typescript-eslint/types": 5.8.0 - "@typescript-eslint/typescript-estree": 5.8.0 - debug: ^4.3.2 - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 138b1d20a6c204fdd0c93295b4ec667caf6036e74bfeae0b80cfe14c4d50761bb9f469b30d320d2d85757a1b98c2ae7f30d9a788a293afc1ea10b9f3d9fbc8f7 + checksum: 731b27840642b644e65f4ae321ed47e973ffadacd1aa24a19b02b4b298b5bcfbfa16c2d3d034e87a08c3c45f942c5b974f7619cb143eb23fb950f37418dce791 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/parser@npm:5.8.1" +"@typescript-eslint/parser@npm:^5.6.0, @typescript-eslint/parser@npm:^5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/parser@npm:5.9.0" dependencies: - "@typescript-eslint/scope-manager": 5.8.1 - "@typescript-eslint/types": 5.8.1 - "@typescript-eslint/typescript-estree": 5.8.1 + "@typescript-eslint/scope-manager": 5.9.0 + "@typescript-eslint/types": 5.9.0 + "@typescript-eslint/typescript-estree": 5.9.0 debug: ^4.3.2 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: bb1702851ff0ade16a50789c517155557ad7e9b8e5e9c4553aad52fedbc8f94acaade1dc5ba12a96b54a13a68dfea13955ab885aad97cf3c526a8b90880bd8a3 + checksum: ae95a7eb977b7bb4eec98357577b043d8ba48d47ae43ec18eadd350336b485ce91ac969b92e22143cc77797cc96cf37598d2bddcdd974d45fb3ec4f01b53b92a languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.8.0": - version: 5.8.0 - resolution: "@typescript-eslint/scope-manager@npm:5.8.0" +"@typescript-eslint/scope-manager@npm:5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/scope-manager@npm:5.9.0" dependencies: - "@typescript-eslint/types": 5.8.0 - "@typescript-eslint/visitor-keys": 5.8.0 - checksum: 15f365a491c096104d3279617522375b6084117ac21e52cf04935a1cce192d730785a1e47afd8a8ca9aa907f1f9cd34793610406ce93447addf6854cdfa830f3 + "@typescript-eslint/types": 5.9.0 + "@typescript-eslint/visitor-keys": 5.9.0 + checksum: 46e7ab0cef558e7faf1aa8d122a265e196566c0073292f5b2f9cede1f63f52860be8e4ef90251c15e0922339c15852584cb5337382035baff87f1203c0c8d1b5 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/scope-manager@npm:5.8.1" +"@typescript-eslint/type-utils@npm:5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/type-utils@npm:5.9.0" dependencies: - "@typescript-eslint/types": 5.8.1 - "@typescript-eslint/visitor-keys": 5.8.1 - checksum: d9254018d723aff32fc512b7292737b154367198ab58e0faf814b4ce77d4de20552ed1678f2639b35e480eb5594eb9d5f1d34360885f5e4d80ca8e5a9ccf666c - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:5.8.0": - version: 5.8.0 - resolution: "@typescript-eslint/types@npm:5.8.0" - checksum: eda7a2c4620fd0cd56a81af6f44d8de96eb5912dda69907cd422e3fb5845b45c004a2c50f1896b6573b70f41f175208434d13dd744ea23aec2094ba916578a81 - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/types@npm:5.8.1" - checksum: f9809c2c0f523841adeeb66410911f10492d3df7a912bc3d72304f4edbc5b5cb1a3f5f2a6ded20e8b524cc18e92d2a735fb8b96570e75df669061182932200ef - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:5.8.0": - version: 5.8.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.8.0" - dependencies: - "@typescript-eslint/types": 5.8.0 - "@typescript-eslint/visitor-keys": 5.8.0 + "@typescript-eslint/experimental-utils": 5.9.0 debug: ^4.3.2 - globby: ^11.0.4 - is-glob: ^4.0.3 - semver: ^7.3.5 tsutils: ^3.21.0 + peerDependencies: + eslint: "*" peerDependenciesMeta: typescript: optional: true - checksum: 67f51754d1dea9eafc8d052b67a2d7a3b20e20d97de03fc49615fe70d0373323619dfa5986a8e71cb9b2ec6079fb050049100763b5dbadae52b30c7d11c57ebd + checksum: 787c3277e37f6bbd723ff10aec6ddc61a62860bd2b1d354c4a50c1aec9b479ee4f51be9fd1cdeac2e43e22161481e76409c00e6a4d50549ceaee0c59fc5cd73d languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/typescript-estree@npm:5.8.1" +"@typescript-eslint/types@npm:5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/types@npm:5.9.0" + checksum: 7c4e142600aec266b41418dab1d0cee8cace980b6990692df6522de6eab6705bf515aef36180e4a38c62acb10c92fb474269ac6856a4266d6b035068cd83fad3 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.9.0" dependencies: - "@typescript-eslint/types": 5.8.1 - "@typescript-eslint/visitor-keys": 5.8.1 + "@typescript-eslint/types": 5.9.0 + "@typescript-eslint/visitor-keys": 5.9.0 debug: ^4.3.2 globby: ^11.0.4 is-glob: ^4.0.3 @@ -1540,27 +1468,17 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: e3cfbd088f1e0104b5b38fcc6e400a0d0e72395694406357e478369c4df532aa2accfe2ee77c71854ca9a04e0e3cddbed86388334805c91ca4241b032cbb6d20 - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:5.8.0": - version: 5.8.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.8.0" - dependencies: - "@typescript-eslint/types": 5.8.0 - eslint-visitor-keys: ^3.0.0 - checksum: 03a349d4a577aa128b27d13a16e6e365d18e6aa9f297bc2a632bc2ddae8cfed9cb66c227f87fde9924e9f8a58c40c41df6f537016d037a05fe1908bfa0839d18 + checksum: 71e3f720e335fb08e66950d32b723484aa4d1f4a3163e82259f4be2d11091545070c2e71472be470403cb6f82bf1abe84fa89c1d0b1d47adc8550b3f70aabfb5 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.8.1": - version: 5.8.1 - resolution: "@typescript-eslint/visitor-keys@npm:5.8.1" +"@typescript-eslint/visitor-keys@npm:5.9.0": + version: 5.9.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.9.0" dependencies: - "@typescript-eslint/types": 5.8.1 + "@typescript-eslint/types": 5.9.0 eslint-visitor-keys: ^3.0.0 - checksum: 46567678718a227b34a255a3606e1a2c5190a470dc9493d4c175f57566d2c16b88780fb273ca44f22cab06d45d87b25371215e93b88ac10a475877bd64bdfece + checksum: 34a595b83b0e7d4f387d6c81b272804b94a1a91478c5f856fdfdd227595bf8562bf3f5d732606d10b4522c3f2617d09d4bacd2193f757a324ea66b3144a68903 languageName: node linkType: hard @@ -1599,16 +1517,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.4.1, acorn@npm:^8.6.0": - version: 8.6.0 - resolution: "acorn@npm:8.6.0" - bin: - acorn: bin/acorn - checksum: 9d0de73b73cb6ea8ccd8263a8144d9e2c4b6af90ea0c429997538af0ebbe83c5addecee814b2a7f91f7f615d0bd1547cc7137b3fa236ce058adc64feccee850b - languageName: node - linkType: hard - -"acorn@npm:^8.7.0": +"acorn@npm:^8.4.1, acorn@npm:^8.7.0": version: 8.7.0 resolution: "acorn@npm:8.7.0" bin: @@ -1634,13 +1543,13 @@ __metadata: linkType: hard "agentkeepalive@npm:^4.1.3": - version: 4.1.4 - resolution: "agentkeepalive@npm:4.1.4" + version: 4.2.0 + resolution: "agentkeepalive@npm:4.2.0" dependencies: debug: ^4.1.0 depd: ^1.1.2 humanize-ms: ^1.2.1 - checksum: d49c24d4b333e9507119385895a583872f4f53d62764a89be165926e824056a126955bae4a6d3c6f7cd26f4089621a40f7b27675f7868214d82118f744b9e82d + checksum: 89806f83ceebbcaabf6bd581a8dce4870910fd2a11f66df8f505b4cd4ce4ca5ab9e6eec8d11ce8531a6b60f6748b75b0775e0e2fa33871503ef00d535418a19a languageName: node linkType: hard @@ -1837,25 +1746,25 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^27.4.5": - version: 27.4.5 - resolution: "babel-jest@npm:27.4.5" +"babel-jest@npm:^27.4.6": + version: 27.4.6 + resolution: "babel-jest@npm:27.4.6" dependencies: - "@jest/transform": ^27.4.5 + "@jest/transform": ^27.4.6 "@jest/types": ^27.4.2 "@types/babel__core": ^7.1.14 - babel-plugin-istanbul: ^6.0.0 + babel-plugin-istanbul: ^6.1.1 babel-preset-jest: ^27.4.0 chalk: ^4.0.0 graceful-fs: ^4.2.4 slash: ^3.0.0 peerDependencies: "@babel/core": ^7.8.0 - checksum: 986601fd143e6bdd9b9c176ade5c1f93a63e38beba511527183fec5f1041920f1262fcb3f87e8660c85fc6cc731d5d49570b35d54c31427644c6849caa137d89 + checksum: fc839d5e8788170e68c8cbde9466fdf1c4fc740a947ba0728e1933ade7ad6fe744c9276d86207f093b64e9cf72a1fdd756fbc44c21034282f01832338e7a8a80 languageName: node linkType: hard -"babel-plugin-istanbul@npm:^6.0.0": +"babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" dependencies: @@ -2046,16 +1955,16 @@ __metadata: linkType: hard "camelcase@npm:^6.2.0": - version: 6.2.1 - resolution: "camelcase@npm:6.2.1" - checksum: d876272ef76391ebf8442fb7ea1d77e80ae179ce1339e021a8731b4895fd190dc19e148e045469cff5825d4c089089f3fff34d804d3f49115d55af97dd6ac0af + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d languageName: node linkType: hard "caniuse-lite@npm:^1.0.30001286": - version: 1.0.30001292 - resolution: "caniuse-lite@npm:1.0.30001292" - checksum: 930d02514769243f26033919f56536a307db83bba933374e6955c6678878fe8a8105051796868947d230f31d237b782fa2cf7390b84b69b555077add12469966 + version: 1.0.30001298 + resolution: "caniuse-lite@npm:1.0.30001298" + checksum: 43566732d1b8746e3dfae57f558471c701b26b2c166fb9fc53ad750d83128b2eb680a5e08233717e64055779b408c72d3b2cfad6d4ae33a3e647a11dc1c0d515 languageName: node linkType: hard @@ -2375,13 +2284,13 @@ __metadata: linkType: hard "conventional-changelog-conventionalcommits@npm:^4.3.1, conventional-changelog-conventionalcommits@npm:^4.5.0": - version: 4.6.2 - resolution: "conventional-changelog-conventionalcommits@npm:4.6.2" + version: 4.6.3 + resolution: "conventional-changelog-conventionalcommits@npm:4.6.3" dependencies: compare-func: ^2.0.0 lodash: ^4.17.15 q: ^1.5.1 - checksum: d8e3e1f8cac9dc5d7989f41e91cfe3804e37f4a3775c3ab99028f4113c20c8a9c12e32213a8d69dd537d9b328d7f4d883d73b112933d142da281740701a4dda7 + checksum: 7b8e8a21ebb56f9aaa510e12917b7c609202072c3e71089e0a09630c37c2e8146cdb04364809839b0e3eb55f807fe84d03b2079500b37f6186d505848be5c562 languageName: node linkType: hard @@ -2461,12 +2370,12 @@ __metadata: linkType: hard "conventional-changelog-writer@npm:^5.0.0": - version: 5.0.0 - resolution: "conventional-changelog-writer@npm:5.0.0" + version: 5.0.1 + resolution: "conventional-changelog-writer@npm:5.0.1" dependencies: conventional-commits-filter: ^2.0.7 dateformat: ^3.0.0 - handlebars: ^4.7.6 + handlebars: ^4.7.7 json-stringify-safe: ^5.0.1 lodash: ^4.17.15 meow: ^8.0.0 @@ -2475,7 +2384,7 @@ __metadata: through2: ^4.0.0 bin: conventional-changelog-writer: cli.js - checksum: c310b949d354688b971f576c92cac77f11540fee56dccb990169e94e4fc42e40245d2c381f826b7d781deb04d4f7e01701cc29bdd1c3d3cdf8817e8b7a80ea18 + checksum: 5c0129db44577f14b1f8de225b62a392a9927ba7fe3422cb21ad71a771b8472bd03badb7c87cb47419913abc3f2ce3759b69f59550cdc6f7a7b0459015b3b44c languageName: node linkType: hard @@ -2516,8 +2425,8 @@ __metadata: linkType: hard "conventional-commits-parser@npm:^3.2.0, conventional-commits-parser@npm:^3.2.2": - version: 3.2.3 - resolution: "conventional-commits-parser@npm:3.2.3" + version: 3.2.4 + resolution: "conventional-commits-parser@npm:3.2.4" dependencies: JSONStream: ^1.0.4 is-text-path: ^1.0.1 @@ -2527,7 +2436,7 @@ __metadata: through2: ^4.0.0 bin: conventional-commits-parser: cli.js - checksum: 0f57b5cb7cb359eb49e6807cfd82b27cbe9ac30ec580b20ad7e79575561183110532a6c2e6328ce6c4cd05c01458b9bb781f1f6653b14560f7c509b87b0e9ac7 + checksum: 1627ff203bc9586d89e47a7fe63acecf339aba74903b9114e23d28094f79d4e2d6389bf146ae561461dcba8fc42e7bc228165d2b173f15756c43f1d32bc50bfd languageName: node linkType: hard @@ -2566,8 +2475,8 @@ __metadata: linkType: hard "cosmiconfig-typescript-loader@npm:^1.0.0": - version: 1.0.2 - resolution: "cosmiconfig-typescript-loader@npm:1.0.2" + version: 1.0.3 + resolution: "cosmiconfig-typescript-loader@npm:1.0.3" dependencies: cosmiconfig: ^7 ts-node: ^10.4.0 @@ -2575,7 +2484,7 @@ __metadata: "@types/node": "*" cosmiconfig: ">=7" typescript: ">=3" - checksum: 9587aa6f80f846799cace916e093f8bff0fb7e48e35eea8afbd50a47c3754b046f8169f81f786de7624c9d93794a73bcdf3b12f77f50420ff53eec02da5743b9 + checksum: 04ff1b23298d1e53d98266668fee8a433cd88b1d22a0bbf1cbfa90fdf0a73f8c68538ad628152dd6ea37eb4651bdf892fb1118fa602c87a009ffce256f43fe4b languageName: node linkType: hard @@ -2805,9 +2714,9 @@ __metadata: languageName: node linkType: hard -"discord.js@npm:^13.5.0": - version: 13.5.0 - resolution: "discord.js@npm:13.5.0" +"discord.js@npm:^13.5.1": + version: 13.5.1 + resolution: "discord.js@npm:13.5.1" dependencies: "@discordjs/builders": ^0.11.0 "@discordjs/collection": ^0.4.0 @@ -2818,7 +2727,7 @@ __metadata: form-data: ^4.0.0 node-fetch: ^2.6.1 ws: ^8.4.0 - checksum: 2c4e38b94ef2f19b8d63829cae753fac27e4aba06599871977499f04bb7384f03dbd158562967c624918a183396b592ef9d6c63c3c5e65e76eb4c62f398daaf0 + checksum: bb38d2675688838b183ce7f273ac322b10b8eec9d38b44397334b5a9277efcfd25b04ef9393dcd805b00945690abc1960bc849181c53f40580729f7542df10b5 languageName: node linkType: hard @@ -2851,9 +2760,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.4.17": - version: 1.4.28 - resolution: "electron-to-chromium@npm:1.4.28" - checksum: ac3dcd1de0de39fc3ad4df2ca7603a4dce683be7c52896c5bf094157fb0452f261639d3b2984aea783d4467ae05ee5af08435be7a35ff80323bdf521c3eb50db + version: 1.4.38 + resolution: "electron-to-chromium@npm:1.4.38" + checksum: 09e63e4f4457c97c9124379c3254dcc55d281004eeefe30d386814872c88817900e2c537c045e2cdd2bc209ad7cd8e4651f8c9e0ec7beefb0f848011620ab089 languageName: node linkType: hard @@ -3027,55 +2936,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.4.1": - version: 8.5.0 - resolution: "eslint@npm:8.5.0" - dependencies: - "@eslint/eslintrc": ^1.0.5 - "@humanwhocodes/config-array": ^0.9.2 - ajv: ^6.10.0 - chalk: ^4.0.0 - cross-spawn: ^7.0.2 - debug: ^4.3.2 - doctrine: ^3.0.0 - enquirer: ^2.3.5 - escape-string-regexp: ^4.0.0 - eslint-scope: ^7.1.0 - eslint-utils: ^3.0.0 - eslint-visitor-keys: ^3.1.0 - espree: ^9.2.0 - esquery: ^1.4.0 - esutils: ^2.0.2 - fast-deep-equal: ^3.1.3 - file-entry-cache: ^6.0.1 - functional-red-black-tree: ^1.0.1 - glob-parent: ^6.0.1 - globals: ^13.6.0 - ignore: ^4.0.6 - import-fresh: ^3.0.0 - imurmurhash: ^0.1.4 - is-glob: ^4.0.0 - js-yaml: ^4.1.0 - json-stable-stringify-without-jsonify: ^1.0.1 - levn: ^0.4.1 - lodash.merge: ^4.6.2 - minimatch: ^3.0.4 - natural-compare: ^1.4.0 - optionator: ^0.9.1 - progress: ^2.0.0 - regexpp: ^3.2.0 - semver: ^7.2.1 - strip-ansi: ^6.0.1 - strip-json-comments: ^3.1.0 - text-table: ^0.2.0 - v8-compile-cache: ^2.0.3 - bin: - eslint: bin/eslint.js - checksum: c1a9e26070520a308cc30b62ba0d37d5b115ed23987a93219819537bdea9398e6ebe57c27d97be36ecc83b5162c72e82ecb0a9e5b44b7992980f9be90eb5c4b3 - languageName: node - linkType: hard - -"eslint@npm:^8.6.0": +"eslint@npm:^8.4.1, eslint@npm:^8.6.0": version: 8.6.0 resolution: "eslint@npm:8.6.0" dependencies: @@ -3123,18 +2984,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.2.0": - version: 9.2.0 - resolution: "espree@npm:9.2.0" - dependencies: - acorn: ^8.6.0 - acorn-jsx: ^5.3.1 - eslint-visitor-keys: ^3.1.0 - checksum: ae533a058036e3efeeac43a0ee39c74ab347e2a73bbe2946fba33cc0d84aca657e675bc317ed9afd95338f79d5d5a862afec2f717d2539ae13fa9f1638371761 - languageName: node - linkType: hard - -"espree@npm:^9.3.0": +"espree@npm:^9.2.0, espree@npm:^9.3.0": version: 9.3.0 resolution: "espree@npm:9.3.0" dependencies: @@ -3244,17 +3094,15 @@ __metadata: languageName: node linkType: hard -"expect@npm:^27.4.2": - version: 27.4.2 - resolution: "expect@npm:27.4.2" +"expect@npm:^27.4.6": + version: 27.4.6 + resolution: "expect@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 - ansi-styles: ^5.0.0 jest-get-type: ^27.4.0 - jest-matcher-utils: ^27.4.2 - jest-message-util: ^27.4.2 - jest-regex-util: ^27.4.0 - checksum: 5eba0f348fd234420d7b4f09968d30d0b19e9e73579ad060e5e635be879671dfb9bed472befe1d5fe8749b6beefc08beba0e034d5aad2aca11e4d5ac43873326 + jest-matcher-utils: ^27.4.6 + jest-message-util: ^27.4.6 + checksum: 593eaa8ff34320f9a70f961bc25eeae932df4f48ebcc5ecc1033f1cddffd286fc42a2f312929222541cec1077de2604ff4fc6e97012afcbd36b333bfaba82f7f languageName: node linkType: hard @@ -3283,16 +3131,16 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.1.1": - version: 3.2.7 - resolution: "fast-glob@npm:3.2.7" +"fast-glob@npm:^3.2.9": + version: 3.2.9 + resolution: "fast-glob@npm:3.2.9" dependencies: "@nodelib/fs.stat": ^2.0.2 "@nodelib/fs.walk": ^1.2.3 glob-parent: ^5.1.2 merge2: ^1.3.0 micromatch: ^4.0.4 - checksum: 2f4708ff112d2b451888129fdd9a0938db88b105b0ddfd043c064e3c4d3e20eed8d7c7615f7565fee660db34ddcf08a2db1bf0ab3c00b87608e4719694642d78 + checksum: 436cb7fa1df2c7f9d63292d309378553d286169635e487d4b8def5094ce3288bf9eef8e7c34b8ec2249668ebe24330d248fa93e90b7a8cbbcd3bd786523f7daa languageName: node linkType: hard @@ -3630,8 +3478,8 @@ __metadata: linkType: hard "git-raw-commits@npm:^2.0.0, git-raw-commits@npm:^2.0.8": - version: 2.0.10 - resolution: "git-raw-commits@npm:2.0.10" + version: 2.0.11 + resolution: "git-raw-commits@npm:2.0.11" dependencies: dargs: ^7.0.0 lodash: ^4.17.15 @@ -3640,7 +3488,7 @@ __metadata: through2: ^4.0.0 bin: git-raw-commits: cli.js - checksum: 66e2d7b4cdeff946ac639e1bba37f5dcbd9f5c9245348b31e027e4529f6b6733d23f75768d285d5f29c1f08d3485705a4932300a81a45b77b660fe3ce6089c29 + checksum: c178af43633684106179793b6e3473e1d2bb50bb41d04e2e285ea4eef342ca4090fee6bc8a737552fde879d22346c90de5c49f18c719a0f38d4c934f258a0f79 languageName: node linkType: hard @@ -3771,27 +3619,27 @@ __metadata: linkType: hard "globby@npm:^11.0.4": - version: 11.0.4 - resolution: "globby@npm:11.0.4" + version: 11.1.0 + resolution: "globby@npm:11.1.0" dependencies: array-union: ^2.1.0 dir-glob: ^3.0.1 - fast-glob: ^3.1.1 - ignore: ^5.1.4 - merge2: ^1.3.0 + fast-glob: ^3.2.9 + ignore: ^5.2.0 + merge2: ^1.4.1 slash: ^3.0.0 - checksum: d3e02d5e459e02ffa578b45f040381c33e3c0538ed99b958f0809230c423337999867d7b0dbf752ce93c46157d3bbf154d3fff988a93ccaeb627df8e1841775b + checksum: b4be8885e0cfa018fc783792942d53926c35c50b3aefd3fdcfb9d22c627639dc26bd2327a40a0b74b074100ce95bb7187bfeae2f236856aa3de183af7a02aea6 languageName: node linkType: hard "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": - version: 4.2.8 - resolution: "graceful-fs@npm:4.2.8" - checksum: 5d224c8969ad0581d551dfabdb06882706b31af2561bd5e2034b4097e67cc27d05232849b8643866585fd0a41c7af152950f8776f4dd5579e9853733f31461c6 + version: 4.2.9 + resolution: "graceful-fs@npm:4.2.9" + checksum: 68ea4e07ff2c041ada184f9278b830375f8e0b75154e3f080af6b70f66172fabb4108d19b3863a96b53fc068a310b9b6493d86d1291acc5f3861eb4b79d26ad6 languageName: node linkType: hard -"handlebars@npm:^4.7.6": +"handlebars@npm:^4.7.7": version: 4.7.7 resolution: "handlebars@npm:4.7.7" dependencies: @@ -3863,11 +3711,11 @@ __metadata: linkType: hard "hosted-git-info@npm:^4.0.0, hosted-git-info@npm:^4.0.1": - version: 4.0.2 - resolution: "hosted-git-info@npm:4.0.2" + version: 4.1.0 + resolution: "hosted-git-info@npm:4.1.0" dependencies: lru-cache: ^6.0.0 - checksum: d1b2d7720398ce96a788bd38d198fbddce089a2381f63cfb01743e6c7e5aed656e5547fe74090fb9fe53b2cb785b0e8c9ebdddadff48ed26bb471dd23cd25458 + checksum: c3f87b3c2f7eb8c2748c8f49c0c2517c9a95f35d26f4bf54b2a8cba05d2e668f3753548b6ea366b18ec8dadb4e12066e19fa382a01496b0ffa0497eb23cbe461 languageName: node linkType: hard @@ -3963,7 +3811,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.4, ignore@npm:^5.1.8": +"ignore@npm:^5.1.4, ignore@npm:^5.1.8, ignore@npm:^5.2.0": version: 5.2.0 resolution: "ignore@npm:5.2.0" checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77 @@ -3981,14 +3829,14 @@ __metadata: linkType: hard "import-local@npm:^3.0.2": - version: 3.0.3 - resolution: "import-local@npm:3.0.3" + version: 3.1.0 + resolution: "import-local@npm:3.1.0" dependencies: pkg-dir: ^4.2.0 resolve-cwd: ^3.0.0 bin: import-local-fixture: fixtures/cli.js - checksum: 38ae57d35e7fd5f63b55895050c798d4dd590e4e2337e9ffa882fb3ea7a7716f3162c7300e382e0a733ca5d07b389fadff652c00fa7b072d5cb6ea34ca06b179 + checksum: bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd languageName: node linkType: hard @@ -4079,12 +3927,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.2.0, is-core-module@npm:^2.5.0": - version: 2.8.0 - resolution: "is-core-module@npm:2.8.0" +"is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.0": + version: 2.8.1 + resolution: "is-core-module@npm:2.8.1" dependencies: has: ^1.0.3 - checksum: f8b52714891e1a6c6577fcb8d5e057bab064a7a30954aab6beb5092e311473eb8da57afd334de4981dc32409ffca998412efc3a2edceb9e397cef6098d21dd91 + checksum: 418b7bc10768a73c41c7ef497e293719604007f88934a6ffc5f7c78702791b8528102fb4c9e56d006d69361549b3d9519440214a74aefc7e0b79e5e4411d377f languageName: node linkType: hard @@ -4227,19 +4075,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^4.0.3": - version: 4.0.3 - resolution: "istanbul-lib-instrument@npm:4.0.3" - dependencies: - "@babel/core": ^7.7.5 - "@istanbuljs/schema": ^0.1.2 - istanbul-lib-coverage: ^3.0.0 - semver: ^6.3.0 - checksum: fa1171d3022b1bb8f6a734042620ac5d9ee7dc80f3065a0bb12863e9f0494d0eefa3d86608fcc0254ab2765d29d7dad8bdc42e5f8df2f9a1fbe85ccc59d76cb9 - languageName: node - linkType: hard - -"istanbul-lib-instrument@npm:^5.0.4": +"istanbul-lib-instrument@npm:^5.0.4, istanbul-lib-instrument@npm:^5.1.0": version: 5.1.0 resolution: "istanbul-lib-instrument@npm:5.1.0" dependencies: @@ -4274,13 +4110,13 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.0.2": - version: 3.1.2 - resolution: "istanbul-reports@npm:3.1.2" +"istanbul-reports@npm:^3.1.3": + version: 3.1.3 + resolution: "istanbul-reports@npm:3.1.3" dependencies: html-escaper: ^2.0.0 istanbul-lib-report: ^3.0.0 - checksum: 052d002f38d74c869bff009e7a59565f45e67f0ea15cb82547b8479fb494b1c85dc84edb9e5bf7925a8f889ef4980e8af2153a0fb016dedf269b050204fe46ed + checksum: ef6e0d9ed05ecab1974c6eb46cc2a12d8570911934192db4ed40cf1978449240ea80aae32c4dd5555b67407cdf860212d1a9e415443af69641aa57ed1da5ebbb languageName: node linkType: hard @@ -4295,47 +4131,47 @@ __metadata: languageName: node linkType: hard -"jest-circus@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-circus@npm:27.4.5" +"jest-circus@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-circus@npm:27.4.6" dependencies: - "@jest/environment": ^27.4.4 - "@jest/test-result": ^27.4.2 + "@jest/environment": ^27.4.6 + "@jest/test-result": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" chalk: ^4.0.0 co: ^4.6.0 dedent: ^0.7.0 - expect: ^27.4.2 + expect: ^27.4.6 is-generator-fn: ^2.0.0 - jest-each: ^27.4.2 - jest-matcher-utils: ^27.4.2 - jest-message-util: ^27.4.2 - jest-runtime: ^27.4.5 - jest-snapshot: ^27.4.5 + jest-each: ^27.4.6 + jest-matcher-utils: ^27.4.6 + jest-message-util: ^27.4.6 + jest-runtime: ^27.4.6 + jest-snapshot: ^27.4.6 jest-util: ^27.4.2 - pretty-format: ^27.4.2 + pretty-format: ^27.4.6 slash: ^3.0.0 stack-utils: ^2.0.3 throat: ^6.0.1 - checksum: 0d9ba909fb73ab17d127208a44e0cd1064ed3fcce3208b7c181b684b00e3504f1edc84119cd14d9c4c8df8957904875bf68e3151303bd06e42345a8635112eb0 + checksum: 00aae02bc4de4afa2144b073c4158a322cb37924d5583ef5caa5cb4badcc8f32474da3a01dd5672e85eda088b92d2b769986b46e36c2c88df0dd6ec0c72bd8c1 languageName: node linkType: hard -"jest-cli@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-cli@npm:27.4.5" +"jest-cli@npm:^27.4.7": + version: 27.4.7 + resolution: "jest-cli@npm:27.4.7" dependencies: - "@jest/core": ^27.4.5 - "@jest/test-result": ^27.4.2 + "@jest/core": ^27.4.7 + "@jest/test-result": ^27.4.6 "@jest/types": ^27.4.2 chalk: ^4.0.0 exit: ^0.1.2 graceful-fs: ^4.2.4 import-local: ^3.0.2 - jest-config: ^27.4.5 + jest-config: ^27.4.7 jest-util: ^27.4.2 - jest-validate: ^27.4.2 + jest-validate: ^27.4.6 prompts: ^2.0.1 yargs: ^16.2.0 peerDependencies: @@ -4345,54 +4181,54 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 8c430614ab058fd612eae402620c784e583477520598aa4f68e9115d5f475a50d6897cdad4c832777ec8964446c5a9f02047cf74bed7e0f090220758eac1cc41 + checksum: bf301039f1c14ef3fa2b7699b7b94328faa5549e34cb1573610c894bedd036ad36e31e6af436e11b3aa85e22e409a05d1fef1624bebc2da7ed416ce969b87307 languageName: node linkType: hard -"jest-config@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-config@npm:27.4.5" +"jest-config@npm:^27.4.7": + version: 27.4.7 + resolution: "jest-config@npm:27.4.7" dependencies: - "@babel/core": ^7.1.0 - "@jest/test-sequencer": ^27.4.5 + "@babel/core": ^7.8.0 + "@jest/test-sequencer": ^27.4.6 "@jest/types": ^27.4.2 - babel-jest: ^27.4.5 + babel-jest: ^27.4.6 chalk: ^4.0.0 ci-info: ^3.2.0 deepmerge: ^4.2.2 glob: ^7.1.1 graceful-fs: ^4.2.4 - jest-circus: ^27.4.5 - jest-environment-jsdom: ^27.4.4 - jest-environment-node: ^27.4.4 + jest-circus: ^27.4.6 + jest-environment-jsdom: ^27.4.6 + jest-environment-node: ^27.4.6 jest-get-type: ^27.4.0 - jest-jasmine2: ^27.4.5 + jest-jasmine2: ^27.4.6 jest-regex-util: ^27.4.0 - jest-resolve: ^27.4.5 - jest-runner: ^27.4.5 + jest-resolve: ^27.4.6 + jest-runner: ^27.4.6 jest-util: ^27.4.2 - jest-validate: ^27.4.2 + jest-validate: ^27.4.6 micromatch: ^4.0.4 - pretty-format: ^27.4.2 + pretty-format: ^27.4.6 slash: ^3.0.0 peerDependencies: ts-node: ">=9.0.0" peerDependenciesMeta: ts-node: optional: true - checksum: 8b166404959d368c49573b8d3e9ff5537557413a96aa41e05824f01147db1525168489ae3f1f028525a587bd724f718f9c77f1256351c48cf0e3c766a86292cb + checksum: 23d5bacc483b2674d6efcd6bfc66bcde7c2b428511b50d17a22a2750d85bfc23753f9e41f504411e411e848e34ec61244bdae9da8782df4ada6e284106f71a4d languageName: node linkType: hard -"jest-diff@npm:^27.0.0, jest-diff@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-diff@npm:27.4.2" +"jest-diff@npm:^27.0.0, jest-diff@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-diff@npm:27.4.6" dependencies: chalk: ^4.0.0 diff-sequences: ^27.4.0 jest-get-type: ^27.4.0 - pretty-format: ^27.4.2 - checksum: e5bcdb4f27747795b74a56d56a9545d7fc8f1671a1251d580aea1a7a52df5db044f62ec24f2abc68305f0226d918a443f3b88d9a82f8d0dc4aaa079b621ab091 + pretty-format: ^27.4.6 + checksum: cf6b7e80e3c64a7c71ab209c0325bbda175991aed985ecee7652df9d6540e4959089038e208c04ab05391c9ddf07adc72f0c8c26cc4cee6fa17f76f500e2bf43 languageName: node linkType: hard @@ -4405,16 +4241,16 @@ __metadata: languageName: node linkType: hard -"jest-each@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-each@npm:27.4.2" +"jest-each@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-each@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 chalk: ^4.0.0 jest-get-type: ^27.4.0 jest-util: ^27.4.2 - pretty-format: ^27.4.2 - checksum: cdc89e68fb3a746b2dcb62a8d05dd6fb15bde47743575bc795ee4123c9e2418f0c99220a9aa96dba94889fb880986158665f33f9c77e6007645ef7d3990ae8e1 + pretty-format: ^27.4.6 + checksum: cce85a14a4c3a37733e75da2352e767c6eef923181e0c884eb9f86253ed417de0454da5117ebfbc1fcabdf109a305b1dbbf9b71a5712da8b6d79fde1f73a9b75 languageName: node linkType: hard @@ -4425,17 +4261,17 @@ __metadata: languageName: node linkType: hard -"jest-environment-node@npm:^27.4.4": - version: 27.4.4 - resolution: "jest-environment-node@npm:27.4.4" +"jest-environment-node@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-environment-node@npm:27.4.6" dependencies: - "@jest/environment": ^27.4.4 - "@jest/fake-timers": ^27.4.2 + "@jest/environment": ^27.4.6 + "@jest/fake-timers": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" - jest-mock: ^27.4.2 + jest-mock: ^27.4.6 jest-util: ^27.4.2 - checksum: 12de67100d35dcdab012220d5c9663e3ad6ac0b164b0a89e998a30c41b71c96abd77256f4fbfcd0ec48f8acb1dbb084050a5d17fe0ad4b4a81e311e05b54a89d + checksum: 3f146e7819f65b1dc0252573cddadc8c565a566ddf7c06c93eded51cccfc55f4765373fb2aaafeb4d8b76ec62b062e1bd4f1da6b9f57429af6789ef8bbada3cb languageName: node linkType: hard @@ -4446,9 +4282,9 @@ __metadata: languageName: node linkType: hard -"jest-haste-map@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-haste-map@npm:27.4.5" +"jest-haste-map@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-haste-map@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 "@types/graceful-fs": ^4.1.2 @@ -4460,13 +4296,13 @@ __metadata: jest-regex-util: ^27.4.0 jest-serializer: ^27.4.0 jest-util: ^27.4.2 - jest-worker: ^27.4.5 + jest-worker: ^27.4.6 micromatch: ^4.0.4 walker: ^1.0.7 dependenciesMeta: fsevents: optional: true - checksum: acd593ec33b028169c7bf753a5c92eabdb05f87ba9f14e33fe24a4adc1e0a1ff4be0c4757a57a82413263ebbb6b567708b4f3019cb4df899d2d07fcec64bd75a + checksum: 07a336e9dba9e7308f16c8b8e037dcc80eb346b0f68cbb6bd1badf97abb104da12c305b411549a5ac0bd4e634b61f9d12e0b5ac2ae8e8bea08952a5fe1a6e82e languageName: node linkType: hard @@ -4477,31 +4313,31 @@ __metadata: languageName: node linkType: hard -"jest-leak-detector@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-leak-detector@npm:27.4.2" +"jest-leak-detector@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-leak-detector@npm:27.4.6" dependencies: jest-get-type: ^27.4.0 - pretty-format: ^27.4.2 - checksum: 093ef57aa6f5563ed5e2c0bce31f8d2ac65438c5d917457dd9a392bf11956a976b55ef2b536cf593b1d65283430305cb6d26e97b064a5c140146346103e74184 + pretty-format: ^27.4.6 + checksum: 4259400403d51b1297b9ab05c1342345c4a93a77c99447b061192ed81b56efcbdd28a03914c9f97670d2f3498bdc368712575d6218b02e3af1656b7db507d3bf languageName: node linkType: hard -"jest-matcher-utils@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-matcher-utils@npm:27.4.2" +"jest-matcher-utils@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-matcher-utils@npm:27.4.6" dependencies: chalk: ^4.0.0 - jest-diff: ^27.4.2 + jest-diff: ^27.4.6 jest-get-type: ^27.4.0 - pretty-format: ^27.4.2 - checksum: 7dd9d2f1f7107d5919af170f9d3e2a08890ce05ee63f6fc3a24e6c8fa9672f99ed107377ae7c6d4d0966a77fa35a3da929465b019b6f1be8cf7e0845806bceb3 + pretty-format: ^27.4.6 + checksum: 445a8cc9eaa7cb08653a10cfc4f109eca76a97d1b1d3a01067bd77efa9cb3a554b74c7402a4c9d5083b21e11218e1515ef538faa47fa47c282072b4825f6b307 languageName: node linkType: hard -"jest-message-util@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-message-util@npm:27.4.2" +"jest-message-util@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-message-util@npm:27.4.6" dependencies: "@babel/code-frame": ^7.12.13 "@jest/types": ^27.4.2 @@ -4509,20 +4345,20 @@ __metadata: chalk: ^4.0.0 graceful-fs: ^4.2.4 micromatch: ^4.0.4 - pretty-format: ^27.4.2 + pretty-format: ^27.4.6 slash: ^3.0.0 stack-utils: ^2.0.3 - checksum: c08ef1c8c1a2001c2f38d6ad3717a6e188b8b25c79b8bd87f2800b9c046f50f33bcd6ab1a9b5a5cc3218b40cf60f37d0583aa0b36ea870c8f100ba0ca7a3c479 + checksum: 1fdd542d091dbf7aa63a484feead97a921e3c4d6db3784fe2e6d83e9110ac06de5691fdc043da991ca1d0ce5d179ea8266c8d93b388f4bba7d80a267fdd946df languageName: node linkType: hard -"jest-mock@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-mock@npm:27.4.2" +"jest-mock@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-mock@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 "@types/node": "*" - checksum: 4ad4a870ec771410b708e955ef2526e7becb91a1d19c4699dcf8fe43a9f6d1231e0c47b87d6b80ee9ad3194ad54dc9abf158588a4a542ad9f9ce8c23eda6048e + checksum: 34df5ec502fa0db5ef36e2b2e96a522de730e7be907c6df5d4ec8ab1292d9be71f1e269e8bcdafd020239edaf3ca6f9c464eb0b4aca6986420a1f392976fc0ab languageName: node linkType: hard @@ -4545,43 +4381,43 @@ __metadata: languageName: node linkType: hard -"jest-resolve-dependencies@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-resolve-dependencies@npm:27.4.5" +"jest-resolve-dependencies@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-resolve-dependencies@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 jest-regex-util: ^27.4.0 - jest-snapshot: ^27.4.5 - checksum: 1fc16cb7c8df130420732184cd87a2c8ae6bf6cbb37d61dd69fddf69ab5ab2be50774962ce4b477b915fa1cc3dc69cb1830b6a18bd1b33c3c1a9c40e43cb11ce + jest-snapshot: ^27.4.6 + checksum: c644adb74a602c8c08f90256c9a5c519434cd213a02a6f427425003f9ab026c12860527eb67cf624aa6717c410fa92aee66662d212c0ffbb73f80e2711ffb7a4 languageName: node linkType: hard -"jest-resolve@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-resolve@npm:27.4.5" +"jest-resolve@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-resolve@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 chalk: ^4.0.0 graceful-fs: ^4.2.4 - jest-haste-map: ^27.4.5 + jest-haste-map: ^27.4.6 jest-pnp-resolver: ^1.2.2 jest-util: ^27.4.2 - jest-validate: ^27.4.2 + jest-validate: ^27.4.6 resolve: ^1.20.0 resolve.exports: ^1.1.0 slash: ^3.0.0 - checksum: 57d619ed1ab4ba5d1b079f9ca3e93c7d9bcc9faa195b617fda6155cbce6eb48c234a957f41f7feee43740b4a5b50ebec8aea61023f766ac4b2eb6ff946c76025 + checksum: 69b765660ee2dd71542953fbe5f6fc9ee3590a4829376e00d955f7566d47049ec5e300832bee1530ac85d2946e341558993ab381d3023363058ae6f9d4c10025 languageName: node linkType: hard -"jest-runner@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-runner@npm:27.4.5" +"jest-runner@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-runner@npm:27.4.6" dependencies: - "@jest/console": ^27.4.2 - "@jest/environment": ^27.4.4 - "@jest/test-result": ^27.4.2 - "@jest/transform": ^27.4.5 + "@jest/console": ^27.4.6 + "@jest/environment": ^27.4.6 + "@jest/test-result": ^27.4.6 + "@jest/transform": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" chalk: ^4.0.0 @@ -4589,52 +4425,48 @@ __metadata: exit: ^0.1.2 graceful-fs: ^4.2.4 jest-docblock: ^27.4.0 - jest-environment-jsdom: ^27.4.4 - jest-environment-node: ^27.4.4 - jest-haste-map: ^27.4.5 - jest-leak-detector: ^27.4.2 - jest-message-util: ^27.4.2 - jest-resolve: ^27.4.5 - jest-runtime: ^27.4.5 + jest-environment-jsdom: ^27.4.6 + jest-environment-node: ^27.4.6 + jest-haste-map: ^27.4.6 + jest-leak-detector: ^27.4.6 + jest-message-util: ^27.4.6 + jest-resolve: ^27.4.6 + jest-runtime: ^27.4.6 jest-util: ^27.4.2 - jest-worker: ^27.4.5 + jest-worker: ^27.4.6 source-map-support: ^0.5.6 throat: ^6.0.1 - checksum: 456f5e3c55dfd0fdad21703a26aa2ff729bbcea173a4ac6a6a99f65d77c564ace13a0e53c33b074020d3594dbff831b7f6424f27d99485120c691ee129a6b6f4 + checksum: 4e76117e5373b6eb51c7113f848dbc92bc1e1d2f1302f9530ef9cb6c967eb364836f4a5790f65a437f47debc917bfb696bbc647831292fa8b1b4321f292e721f languageName: node linkType: hard -"jest-runtime@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-runtime@npm:27.4.5" +"jest-runtime@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-runtime@npm:27.4.6" dependencies: - "@jest/console": ^27.4.2 - "@jest/environment": ^27.4.4 - "@jest/globals": ^27.4.4 + "@jest/environment": ^27.4.6 + "@jest/fake-timers": ^27.4.6 + "@jest/globals": ^27.4.6 "@jest/source-map": ^27.4.0 - "@jest/test-result": ^27.4.2 - "@jest/transform": ^27.4.5 + "@jest/test-result": ^27.4.6 + "@jest/transform": ^27.4.6 "@jest/types": ^27.4.2 - "@types/yargs": ^16.0.0 chalk: ^4.0.0 cjs-module-lexer: ^1.0.0 collect-v8-coverage: ^1.0.0 execa: ^5.0.0 - exit: ^0.1.2 glob: ^7.1.3 graceful-fs: ^4.2.4 - jest-haste-map: ^27.4.5 - jest-message-util: ^27.4.2 - jest-mock: ^27.4.2 + jest-haste-map: ^27.4.6 + jest-message-util: ^27.4.6 + jest-mock: ^27.4.6 jest-regex-util: ^27.4.0 - jest-resolve: ^27.4.5 - jest-snapshot: ^27.4.5 + jest-resolve: ^27.4.6 + jest-snapshot: ^27.4.6 jest-util: ^27.4.2 - jest-validate: ^27.4.2 slash: ^3.0.0 strip-bom: ^4.0.0 - yargs: ^16.2.0 - checksum: 3fddd950504e2eee83f13237d8e2321c91237881a04e71cfd5457064eb970a91de3b8560b15ed6dbfc8843aa06151907510842f5f2f8e93b5a172a1d282ae26e + checksum: 64d833c7d7b1d67b53932dc9fd9332aaf43ea1777fc61c3f143515968f066438b3247e4f1a71a7f127b1bedbc7c3124bfc53cb4f026fff5b26e2feda8d35535c languageName: node linkType: hard @@ -4648,35 +4480,33 @@ __metadata: languageName: node linkType: hard -"jest-snapshot@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-snapshot@npm:27.4.5" +"jest-snapshot@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-snapshot@npm:27.4.6" dependencies: "@babel/core": ^7.7.2 "@babel/generator": ^7.7.2 - "@babel/parser": ^7.7.2 "@babel/plugin-syntax-typescript": ^7.7.2 "@babel/traverse": ^7.7.2 "@babel/types": ^7.0.0 - "@jest/transform": ^27.4.5 + "@jest/transform": ^27.4.6 "@jest/types": ^27.4.2 "@types/babel__traverse": ^7.0.4 "@types/prettier": ^2.1.5 babel-preset-current-node-syntax: ^1.0.0 chalk: ^4.0.0 - expect: ^27.4.2 + expect: ^27.4.6 graceful-fs: ^4.2.4 - jest-diff: ^27.4.2 + jest-diff: ^27.4.6 jest-get-type: ^27.4.0 - jest-haste-map: ^27.4.5 - jest-matcher-utils: ^27.4.2 - jest-message-util: ^27.4.2 - jest-resolve: ^27.4.5 + jest-haste-map: ^27.4.6 + jest-matcher-utils: ^27.4.6 + jest-message-util: ^27.4.6 jest-util: ^27.4.2 natural-compare: ^1.4.0 - pretty-format: ^27.4.2 + pretty-format: ^27.4.6 semver: ^7.3.2 - checksum: c5dcb1ccb95feb8773fc64b6d21d28fc8e8d2cf53bfde74247b3d34a83936a9b92492416d447d4e559e7b2ce39e442e4ee4a266d2f54c9ab8ab686eb16d1c8f4 + checksum: c7a1ae993ae7334277c61e6d645efedefce53ca212498ae766ea28efa46287559a56d2bd2edaaead8476191a45adbb1354df5367dfd223763b5a66751bfbda14 languageName: node linkType: hard @@ -4694,53 +4524,53 @@ __metadata: languageName: node linkType: hard -"jest-validate@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-validate@npm:27.4.2" +"jest-validate@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-validate@npm:27.4.6" dependencies: "@jest/types": ^27.4.2 camelcase: ^6.2.0 chalk: ^4.0.0 jest-get-type: ^27.4.0 leven: ^3.1.0 - pretty-format: ^27.4.2 - checksum: 32d3d5e7945d3450c7d7374882b8a0e6e5481b759cf67f765578424d690594875009a5f9dd2626d7b12e4c816b61eb7d5e19f1b0593cc269f37d527eb4fd1a15 + pretty-format: ^27.4.6 + checksum: d3578030eadd872b99e65dac24d9ca755f2a2483f8344d9e575ea6034c6cb5ed5bcf7a4aa4f1050ab0080d5a8d0b0efd31c911514f27820b871a636a97dc196c languageName: node linkType: hard -"jest-watcher@npm:^27.4.2": - version: 27.4.2 - resolution: "jest-watcher@npm:27.4.2" +"jest-watcher@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-watcher@npm:27.4.6" dependencies: - "@jest/test-result": ^27.4.2 + "@jest/test-result": ^27.4.6 "@jest/types": ^27.4.2 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 jest-util: ^27.4.2 string-length: ^4.0.1 - checksum: f6078349e5c4638b8778dfad0e846aba5665f3bf1f8e8565c436533a5effd8592123b99f950d534965d841edef391ecd86849f5d4ea7d737f99daa7ecfd643cb + checksum: bb9c0a34dcc690cef6430c275e81213620bc4ba6337e42302efa51666ac06781e9f6f50c930332396e4e8cd8cc47de8fb2e8de57da0f7e35a246b0206dde1cd3 languageName: node linkType: hard -"jest-worker@npm:^27.4.5": - version: 27.4.5 - resolution: "jest-worker@npm:27.4.5" +"jest-worker@npm:^27.4.6": + version: 27.4.6 + resolution: "jest-worker@npm:27.4.6" dependencies: "@types/node": "*" merge-stream: ^2.0.0 supports-color: ^8.0.0 - checksum: eb0b6be412103299c3d8643ad26daf862826ca841bd2a3ff47d2d931804ab7d7f0db2fcdea7dbf47ce8eacb7742b3f2586c2d6ebdaa8d0ac77c65f7b698e7683 + checksum: 105bcdf5c66700bbfe352bc09476629ca0858cfa819fcc1a37ea76660f0168d586c6e77aee8ea91eded5a20f40f331a0a81e503b5ba19f7b566204406b239466 languageName: node linkType: hard -"jest@npm:^27.4.5": - version: 27.4.5 - resolution: "jest@npm:27.4.5" +"jest@npm:^27.4.7": + version: 27.4.7 + resolution: "jest@npm:27.4.7" dependencies: - "@jest/core": ^27.4.5 + "@jest/core": ^27.4.7 import-local: ^3.0.2 - jest-cli: ^27.4.5 + jest-cli: ^27.4.7 peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -4748,7 +4578,7 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 57ee4be68650dd1f89e077cca48813d824779a07626e84178c672727ace1ef3cd489f124a27dc02b88601774413330e6d35080b11919efa6460ee61d378c6610 + checksum: 28ce948b30c074907393f37553acac4422d0f60190776e62b3403e4c742d33dd6012e3a20748254a43e38b5b4ce52d813b13a3a5be1d43d6d12429bd08ce1a23 languageName: node linkType: hard @@ -4928,9 +4758,9 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:^12.1.4": - version: 12.1.4 - resolution: "lint-staged@npm:12.1.4" +"lint-staged@npm:^12.1.7": + version: 12.1.7 + resolution: "lint-staged@npm:12.1.7" dependencies: cli-truncate: ^3.1.0 colorette: ^2.0.16 @@ -4947,20 +4777,20 @@ __metadata: yaml: ^1.10.2 bin: lint-staged: bin/lint-staged.js - checksum: f1f829328fbd0878e8c2100fe0968101e94903ce5acf95bae98ec6daa84c4e95f87957f9040782427be2087659f3809626ea42ba53bbe5f0036c4d3806cb313d + checksum: 3d0757706c8b0ca3f004bb2ffa89a9d5fc97ccdebfa4cde29a9636c661538780e194b99cbc5e4b8b89b13986187b33c97e009b09014511bbd45ee05ba557d0eb languageName: node linkType: hard "listr2@npm:^3.13.5": - version: 3.13.5 - resolution: "listr2@npm:3.13.5" + version: 3.14.0 + resolution: "listr2@npm:3.14.0" dependencies: cli-truncate: ^2.1.0 colorette: ^2.0.16 log-update: ^4.0.0 p-map: ^4.0.0 rfdc: ^1.3.0 - rxjs: ^7.4.0 + rxjs: ^7.5.1 through: ^2.3.8 wrap-ansi: ^7.0.0 peerDependencies: @@ -4968,7 +4798,7 @@ __metadata: peerDependenciesMeta: enquirer: optional: true - checksum: c20203060b2deb441d547d753b63fec53d7fe1455f2bce60926ce941a730413455178038abe37f2cdbf490002778d284585d247c39a30cc3c5b08b7151d85386 + checksum: fdb8b2d6bdf5df9371ebd5082bee46c6d0ca3d1e5f2b11fbb5a127839855d5f3da9d4968fce94f0a5ec67cac2459766abbb1faeef621065ebb1829b11ef9476d languageName: node linkType: hard @@ -5199,7 +5029,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.3.0": +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 @@ -5804,7 +5634,7 @@ __metadata: languageName: node linkType: hard -"path-parse@npm:^1.0.6": +"path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a @@ -5835,9 +5665,9 @@ __metadata: linkType: hard "picomatch@npm:^2.0.4, picomatch@npm:^2.2.3": - version: 2.3.0 - resolution: "picomatch@npm:2.3.0" - checksum: 16818720ea7c5872b6af110760dee856c8e4cd79aed1c7a006d076b1cc09eff3ae41ca5019966694c33fbd2e1cc6ea617ab10e4adac6df06556168f13be3fca2 + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf languageName: node linkType: hard @@ -5855,7 +5685,7 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.1": +"pirates@npm:^4.0.4": version: 4.0.4 resolution: "pirates@npm:4.0.4" checksum: 6b7187d526fd025a2b91e8fd289c78d88c4adc3ea947b9facbe9cb300a896b0ec00f3e77b36a043001695312a8debbf714453495283bd8a4eaad3bc0c38df425 @@ -5896,15 +5726,14 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^27.0.0, pretty-format@npm:^27.4.2": - version: 27.4.2 - resolution: "pretty-format@npm:27.4.2" +"pretty-format@npm:^27.0.0, pretty-format@npm:^27.4.6": + version: 27.4.6 + resolution: "pretty-format@npm:27.4.6" dependencies: - "@jest/types": ^27.4.2 ansi-regex: ^5.0.1 ansi-styles: ^5.0.0 react-is: ^17.0.1 - checksum: 0daaf00c4dcb35493e57d30147e8045d0c45cb47fc4c94e3ab1892401abe939627c39975c77cc81eb2581aaa5b12bf23ef669fa550bec68b396fb79dd8c10afa + checksum: 5eda32e4e47ddd1a9e8fe9ebef519b217ba403eb8bcb804ba551dfb37f87e674472013fcf78480ab535844fdddcc706fac94511eba349bfb94a138a02d1a7a59 languageName: node linkType: hard @@ -6156,22 +5985,28 @@ __metadata: linkType: hard "resolve@npm:^1.10.0, resolve@npm:^1.20.0": - version: 1.20.0 - resolution: "resolve@npm:1.20.0" + version: 1.21.0 + resolution: "resolve@npm:1.21.0" dependencies: - is-core-module: ^2.2.0 - path-parse: ^1.0.6 - checksum: 40cf70b2cde00ef57f99daf2dc63c6a56d6c14a1b7fc51735d06a6f0a3b97cb67b4fb7ef6c747b4e13a7baba83b0ef625d7c4ce92a483cd5af923c3b65fd16fe + is-core-module: ^2.8.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: d7d9092a5c04a048bea16c7e5a2eb605ac3e8363a0cc5644de1fde17d5028e8d5f4343aab1d99bd327b98e91a66ea83e242718150c64dfedcb96e5e7aad6c4f5 languageName: node linkType: hard "resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin": - version: 1.20.0 - resolution: "resolve@patch:resolve@npm%3A1.20.0#~builtin::version=1.20.0&hash=07638b" + version: 1.21.0 + resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin::version=1.21.0&hash=07638b" dependencies: - is-core-module: ^2.2.0 - path-parse: ^1.0.6 - checksum: a0dd7d16a8e47af23afa9386df2dff10e3e0debb2c7299a42e581d9d9b04d7ad5d2c53f24f1e043f7b3c250cbdc71150063e53d0b6559683d37f790b7c8c3cd5 + is-core-module: ^2.8.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: a0a4d1f7409e73190f31f901f8a619960bb3bd4ae38ba3a54c7ea7e1c87758d28a73256bb8d6a35996a903d1bf14f53883f0dcac6c571c063cb8162d813ad26e languageName: node linkType: hard @@ -6227,7 +6062,7 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-dts@npm:^4.0.0": +"rollup-plugin-dts@npm:^4.1.0": version: 4.1.0 resolution: "rollup-plugin-dts@npm:4.1.0" dependencies: @@ -6254,9 +6089,9 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^2.58.0, rollup@npm:^2.62.0": - version: 2.62.0 - resolution: "rollup@npm:2.62.0" +"rollup@npm:^2.63.0": + version: 2.63.0 + resolution: "rollup@npm:2.63.0" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -6264,7 +6099,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 9dfa089a232346bc548bf5110e79e0cf5a2dac6fb9bf3f737a645e72795b4b4a1165d1bf86938f90805c4391e8dd571557afb901aaf81dcb82690c57737ab128 + checksum: 23db16ea9d222ad5ae9620ba51d4f45c834927038c1e43d87f7dd3d240aa54422e51c2660437479af4b771e13f9529df236a3d43a3b9f4229bf241347d5f2c8f languageName: node linkType: hard @@ -6293,12 +6128,12 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.4.0": - version: 7.4.0 - resolution: "rxjs@npm:7.4.0" +"rxjs@npm:^7.5.1": + version: 7.5.1 + resolution: "rxjs@npm:7.5.1" dependencies: - tslib: ~2.1.0 - checksum: 6b33172a760dcad6882fdc836ee8cf1ebe160dd7eaad95c45a12338ffdaa96eb41e48e6c25bbd3d1fdf45075949ff447954bc17a9d01c688558a67967d09c114 + tslib: ^2.1.0 + checksum: 78e3eecb1644dd83adabc8d956f879dca62eb19c8afcd6acac71cf6d94534c33ea201e65387494fdeb1332395cba081e194f5a9699d58a5d48e416b8b52372aa languageName: node linkType: hard @@ -6805,6 +6640,13 @@ __metadata: languageName: node linkType: hard +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae + languageName: node + linkType: hard + "tar@npm:^6.0.2, tar@npm:^6.1.2": version: 6.1.11 resolution: "tar@npm:6.1.11" @@ -7017,20 +6859,13 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.3.1": +"tslib@npm:^2.1.0, tslib@npm:^2.3.1": version: 2.3.1 resolution: "tslib@npm:2.3.1" checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 languageName: node linkType: hard -"tslib@npm:~2.1.0": - version: 2.1.0 - resolution: "tslib@npm:2.1.0" - checksum: aa189c8179de0427b0906da30926fd53c59d96ec239dff87d6e6bc831f608df0cbd6f77c61dabc074408bd0aa0b9ae4ec35cb2c15f729e32f37274db5730cb78 - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0"