From 9e9e96b2fec88d89fe9ebec636400196be01ad63 Mon Sep 17 00:00:00 2001 From: Amgelo563 Date: Tue, 21 May 2024 19:52:11 -0500 Subject: [PATCH] feat!: rewrite commands to use DJS builders, allow guild commands and create CommandDeployer --- README.md | 37 +- .../commands/command-customid-codec.mdx | 4 - .../features/commands/command-deployer.mdx | 53 +++ .../features/commands/command-executor.mdx | 4 - .../commands/command-interception.mdx | 4 - .../features/commands/command-manager.mdx | 12 +- .../features/commands/command-overview.mdx | 323 ++++++++++++++---- .../features/commands/command-repository.mdx | 13 +- .../features/commands/command-resolver.mdx | 11 - .../features/commands/command-subscribers.mdx | 11 +- .../commands/context-menu-command.mdx | 66 ++++ .../commands/commands/parent-command.mdx | 38 ++- .../commands/commands/standalone-command.mdx | 118 +------ .../commands/commands/subcommand-group.mdx | 39 ++- .../features/commands/commands/subcommand.mdx | 39 ++- apps/docs/docs/features/events/event-bus.mdx | 7 - .../docs/features/events/event-overview.mdx | 7 - apps/docs/docs/intro.mdx | 49 --- apps/docs/docs/start.mdx | 29 +- apps/docs/docs/welcome.mdx | 52 ++- apps/docs/sidebars.ts | 5 +- apps/docs/src/css/custom.css | 248 +++++++------- packages/core/src/bot/BotOptions.ts | 1 - packages/core/src/customId/CustomIdCodec.ts | 3 +- .../src/features/command/CommandManager.ts | 27 +- .../commands/{abstract => }/Command.ts | 48 ++- .../ContextMenuCommand.ts} | 31 +- .../command/commands/ParentCommand.ts | 17 +- .../command/commands/StandaloneCommand.ts | 43 +-- .../features/command/commands/SubCommand.ts | 15 +- .../command/commands/SubCommandGroup.ts | 12 +- .../command/commands/TopLevelCommand.ts | 10 +- .../commands/abstract/ExecutableCommand.ts | 70 ---- .../{abstract => child}/ChildCommand.ts | 11 +- .../{abstract => child}/ChildableCommand.ts | 31 +- .../executable/AnyExecutableCommand.ts} | 13 +- .../executable/ChatExecutableCommand.ts} | 27 +- .../executable/ExecutableCommand.ts} | 43 +-- .../implements/ImplementsStandaloneCommand.ts | 4 +- .../customId/CommandCustomIdBuilder.ts | 119 ------- .../command/customId/CommandCustomIdCodec.ts | 21 +- .../SerializedCommandData.ts} | 11 +- .../command/data/command/CommandData.ts | 37 -- .../command/deploy/CommandDeployer.ts | 83 +++++ .../ReadonlyCommandDeployer.ts} | 10 +- .../command/error/CommandErrorHandler.ts | 7 +- .../errors/CommandAutocompleteError.ts | 7 +- .../errors/CommandAutocompleteRespondError.ts | 9 +- .../features/command/errors/CommandError.ts | 9 +- .../features/command/events/CommandEvent.ts | 11 +- .../execution/executor/CommandExecutor.ts | 20 +- .../execution/meta/CommandExecutionMeta.ts | 13 +- .../features/command/filter/CommandFilter.ts | 9 +- .../command/middleware/CommandMiddleware.ts | 7 +- .../errors/CommandMiddlewareError.ts | 7 +- .../errors/UncaughtCommandMiddlewareError.ts | 5 +- .../command/repository/CommandRepository.ts | 128 +++---- .../{resolver => resolve}/CommandResolver.ts | 18 +- .../command/resolver/CommandReferenceData.ts | 63 ---- packages/core/src/index.ts | 26 +- packages/framework/src/bot/Bot.ts | 5 +- ...tomIdCodec.ts => AbstractCustomIdCodec.ts} | 12 +- .../customId/IdentifiableCustomIdCodec.ts} | 15 +- .../features/command/DefaultCommandManager.ts | 148 +++++--- .../{abstract => }/AbstractCommand.ts | 36 +- .../commands/AbstractContextMenuCommand.ts | 99 ++++++ .../command/commands/AbstractParentCommand.ts | 70 +++- .../commands/AbstractStandaloneCommand.ts | 85 ++--- .../command/commands/AbstractSubCommand.ts | 60 ++-- .../commands/AbstractSubCommandGroup.ts | 31 +- .../AbstractChildableCommand.ts | 78 +++-- .../AbstractExecutableCommand.ts | 61 +--- .../customId/DefaultCommandCustomIdCodec.ts | 44 +-- .../command/deploy/DefaultCommandDeployer.ts | 219 ++++++++++++ .../DefaultCommandExecutor.ts | 103 ++---- .../command/filter/AbstractCommandFilter.ts | 9 +- .../CommandFilterCheckMiddleware.ts | 9 +- .../middleware/AbstractCommandMiddleware.ts | 13 +- .../repository/DefaultCommandRepository.ts | 282 +++------------ .../DefaultCommandResolver.ts | 64 ++-- .../command/visitor/CommandDataVisitor.ts | 161 --------- .../customId/DefaultSessionCustomIdCodec.ts | 7 +- packages/framework/src/index.ts | 16 +- 83 files changed, 1852 insertions(+), 1970 deletions(-) create mode 100644 apps/docs/docs/features/commands/command-deployer.mdx create mode 100644 apps/docs/docs/features/commands/commands/context-menu-command.mdx delete mode 100644 apps/docs/docs/intro.mdx rename packages/core/src/features/command/commands/{abstract => }/Command.ts (50%) rename packages/core/src/features/command/{data/command/TopLevelCommandData.ts => commands/ContextMenuCommand.ts} (59%) delete mode 100644 packages/core/src/features/command/commands/abstract/ExecutableCommand.ts rename packages/core/src/features/command/commands/{abstract => child}/ChildCommand.ts (82%) rename packages/core/src/features/command/commands/{abstract => child}/ChildableCommand.ts (77%) rename packages/core/src/features/command/{data/command/ParentCommandData.ts => commands/executable/AnyExecutableCommand.ts} (78%) rename packages/core/src/features/command/{data/option/CommandOptionData.ts => commands/executable/ChatExecutableCommand.ts} (63%) rename packages/core/src/features/command/{visitor/CommandVisitor.ts => commands/executable/ExecutableCommand.ts} (50%) delete mode 100644 packages/core/src/features/command/customId/CommandCustomIdBuilder.ts rename packages/core/src/features/command/{data/command/SubCommandGroupData.ts => customId/SerializedCommandData.ts} (84%) delete mode 100644 packages/core/src/features/command/data/command/CommandData.ts create mode 100644 packages/core/src/features/command/deploy/CommandDeployer.ts rename packages/core/src/features/command/{data/command/SubCommandData.ts => deploy/ReadonlyCommandDeployer.ts} (83%) rename packages/core/src/features/command/{resolver => resolve}/CommandResolver.ts (74%) delete mode 100644 packages/core/src/features/command/resolver/CommandReferenceData.ts rename packages/framework/src/customId/{BasicCustomIdCodec.ts => AbstractCustomIdCodec.ts} (93%) rename packages/{core/src/features/command/data/command/StandaloneCommandData.ts => framework/src/customId/IdentifiableCustomIdCodec.ts} (74%) rename packages/framework/src/features/command/commands/{abstract => }/AbstractCommand.ts (71%) create mode 100644 packages/framework/src/features/command/commands/AbstractContextMenuCommand.ts rename packages/framework/src/features/command/commands/{abstract => child}/AbstractChildableCommand.ts (62%) rename packages/framework/src/features/command/commands/{abstract => executable}/AbstractExecutableCommand.ts (67%) create mode 100644 packages/framework/src/features/command/deploy/DefaultCommandDeployer.ts rename packages/framework/src/features/command/{executor => execution}/DefaultCommandExecutor.ts (78%) rename packages/framework/src/features/command/{resolver => resolve}/DefaultCommandResolver.ts (58%) delete mode 100644 packages/framework/src/features/command/visitor/CommandDataVisitor.ts diff --git a/README.md b/README.md index ec18a3e4..be66e840 100644 --- a/README.md +++ b/README.md @@ -27,35 +27,26 @@ satisfy the respective interface. ### Commands -* Complete application command support, with Standalone commands, Parent commands, Subcommands and SubCommand Groups. +* Complete application command support (slash commands or context menus). * Add, remove and update commands (including their children) in runtime. - * Route interactions (buttons, select menus and modal submits) to a command, with an easy API that allows storing and - retrieving extra data from the customId. -* Command and SubCommand option autocompletion support. -* Discord's context menus ([User](https://discord.com/developers/docs/interactions/application-commands#user-commands) - and [Message](https://discord.com/developers/docs/interactions/application-commands#message-commands) commands) - support. -* Replace the interaction listeners with your own for even more custom command handling. -* Deny command executions via `CommandMiddlewares` (for general command filtering) or `CommandFilters` (for - command-specific filtering). -* Handle uncaught command errors with a provided `ErrorHandler`. + * Create components (buttons, select menus and modal submits) that trigger a command, with an easy API that allows + storing and retrieving extra data from the customId. +* Intercept command execution with `CommandMiddlewares` (for general filtering) or `CommandFilters` (for command-specific filtering). +* Use the `CommandExecutionMeta` to provide metadata to your commands from your filter, middleware or subscriber. +* Subscribe to meta events in the `CommandEventBus` like command runs. +* Completely override the command interaction listeners with your own for even more custom interaction handling. +* Handle uncaught command errors on a built-in `ErrorHandler`. ### Events -* Register multiple `EventBuses`, each with their own execution logic. -* Subscribe (and unsubscribe) to events on `EventBuses` via `EventSubscriber` objects or callback functions. -* Enforce emitting and receiving events type safely, including your custom `EventBuses`. -* Use the built-in `BotEventBus` to subscribe to `NyxBot` wide events, such as `botStop`, `commandAdd`, `scheduleRun`, - and many more. -* Store event metadata across subscribers to share information about the event itself. - * The metadata allows marking events as handled so the `EventMiddleware` removes subscribers that don't want to - receive such events. -* Specify an `EventDispatcher` to change the way subscribers are called. By default, two are included: +* `EventBus` and `EventSubscriber` based event handling. +* Enforced type safety when emitting and receiving events when using the provided or your custom `EventBuses`. +* Store event metadata to share information about the event itself across subscribers. +* Specify an `EventDispatcher` to change the way subscribers are called. By default, either: * The `AsyncEventDispatcher` which allows a concurrency limit. * The `SyncEventDispatcher` which allows a sync timeout limit before calling the next subscriber. -* Deny subscriber executions via `EventMiddlewares` (for general filtering) or `SubscriberFilters` (for subscriber - specific filtering). -* Handle subscriber errors via a flexible `ErrorHandler`. +* Intercept subscriber execution with `EventMiddlewares` (for general filtering) or `SubscriberFilters` (for subscriber-specific filtering). +* Handle uncaught subscriber errors on a built-in `ErrorHandler`. ### Schedules diff --git a/apps/docs/docs/features/commands/command-customid-codec.mdx b/apps/docs/docs/features/commands/command-customid-codec.mdx index e32b5889..4bb08a8f 100644 --- a/apps/docs/docs/features/commands/command-customid-codec.mdx +++ b/apps/docs/docs/features/commands/command-customid-codec.mdx @@ -212,8 +212,6 @@ You can create a command custom ID codec by either: ``` ```ts -import { DefaultCommandCustomIdCodec } from '@nyx-discord/framework'; - class MyCommandCustomIdCodec extends DefaultCommandCustomIdCodec { // ... } @@ -231,8 +229,6 @@ const myBot = Bot.create((bot) => ({ ``` ```ts -import { CommandCustomIdCodec } from '@nyx-discord/core'; - class MyCommandCustomIdCodec implements CommandCustomIdCodec { // ... } diff --git a/apps/docs/docs/features/commands/command-deployer.mdx b/apps/docs/docs/features/commands/command-deployer.mdx new file mode 100644 index 00000000..e091d35e --- /dev/null +++ b/apps/docs/docs/features/commands/command-deployer.mdx @@ -0,0 +1,53 @@ +--- +title: ๐Ÿช Command Deployer +--- + +The `CommandDeployer` is the object responsible for deploying commands. It's stored by a `CommandManager`, and you can get +it via `CommandManager#getDeployer()`. + +You can't modify the repository directly since the `CommandManager` returns a `ReadonlyCommandDeployer` type, but the +hidden methods are available at the `CommandManager`. This is because adding, removing and updating a command needs more +logic than just modifying the repository, and the manager is responsible for coordinating this. + +## ๐Ÿ‘ท Creation + +You can create a command deployer by either: + +* Extending `DefaultCommandDeployer` from `@framework` (recommended). +* Implementing the `CommandDeployer` interface from `@core`. + +```mdx-code-block + + +``` + +```ts +class MyCommandDeployer extends DefaultCommandDeployer { + // ... +} + +const myBot = Bot.create((bot) => ({ + commands: DefaultCommandManager.create(bot, client, clientBus, { deployer: myDeployer }), +})) +``` + +```mdx-code-block + + +``` + +```ts +class MyCommandDeployer implements CommandDeployer { + // ... +} + +const myBot = Bot.create((bot) => ({ + commands: DefaultCommandManager.create(bot, client, clientBus, { deployer: myDeployer }), +})) +``` + +```mdx-code-block + + +``` + diff --git a/apps/docs/docs/features/commands/command-executor.mdx b/apps/docs/docs/features/commands/command-executor.mdx index b733f859..a1567181 100644 --- a/apps/docs/docs/features/commands/command-executor.mdx +++ b/apps/docs/docs/features/commands/command-executor.mdx @@ -26,8 +26,6 @@ You can create a custom command executor by either: ``` ```ts -import { DefaultCommandExecutor } from '@nyx-discord/framework'; - class MyCommandExecutor extends DefaultCommandExecutor { // ... } @@ -45,8 +43,6 @@ const myBot = Bot.create((bot) => ({ ``` ```ts -import { CommandExecutor } from '@nyx-discord/core'; - class MyCommandExecutor implements CommandExecutor { // ... } diff --git a/apps/docs/docs/features/commands/command-interception.mdx b/apps/docs/docs/features/commands/command-interception.mdx index 389831f6..7188eac3 100644 --- a/apps/docs/docs/features/commands/command-interception.mdx +++ b/apps/docs/docs/features/commands/command-interception.mdx @@ -58,8 +58,6 @@ You can create your own middleware by either: ``` ```ts -import { AbstractCommandMiddleware } from '@nyx-discord/framework'; - class MyCommandMiddleware extends AbstractCommandMiddleware { public check(command: ExecutableCommand, ...args: CommandExecutionArgs): MiddlewareResponse { return this.true(); @@ -77,8 +75,6 @@ bot.commands.getExecutor().getMiddleware().add(middleware); ``` ```ts -import { CommandMiddleware } from '@nyx-discord/core'; - class MyCommandMiddleware implements CommandMiddleware { // ... } diff --git a/apps/docs/docs/features/commands/command-manager.mdx b/apps/docs/docs/features/commands/command-manager.mdx index 2592d8a4..7e893cec 100644 --- a/apps/docs/docs/features/commands/command-manager.mdx +++ b/apps/docs/docs/features/commands/command-manager.mdx @@ -6,12 +6,12 @@ The `CommandManager` is the object that holds together the nyx event system. It consists of: -* `CommandSubscriberContainer` for managing command-related subscribers. -* `CommandCustomIdProcessor` for processing command customIds. -* `CommandRouter` for routing command interactions to the respective command. -* `CommandExecutor` for executing concrete commands. -* `CommandRepository` for storing the registered commands and managing their registration at Discord. -* `EventBus` for emitting command related events. +* `CommandCustomIdCodec`: De/serializes commands names to/from customId strings, useful for creating message components that will trigger commands. +* `CommandResolver`: Resolves the command data that a given command interaction refers to. +* `CommandExecutor`: Executes command objects, checking its `CommandMiddlewareList` and passing any errors to its `ErrorHandler`. +* `CommandRepository`: Stores all the currently registered commands and their command application mappings. It's also responsible for registering commands at Discord. +* `CommandSubscriptionsContainer`: Stores the [๐Ÿ“ฉ Event Subscribers](../events/event-subscriber) that are subscribed to the [๐Ÿ‘‚ Client event bus](../events/event-manager.mdx#-client-event-bus) to listen for command interactions. +* `EventBus`: An [๐Ÿ“ฃ Event Bus](../events/event-bus) that emits command related events. As well as methods to interact with them. diff --git a/apps/docs/docs/features/commands/command-overview.mdx b/apps/docs/docs/features/commands/command-overview.mdx index 6d21cfe4..128d89ab 100644 --- a/apps/docs/docs/features/commands/command-overview.mdx +++ b/apps/docs/docs/features/commands/command-overview.mdx @@ -17,31 +17,22 @@ said execution to the correspondent command object, all coordinated by a `Comman Specifically, the command related objects are: -* `CommandManager`: The entry point for the command system, holding all the command-related objects and methods that use all of these objects. All objects below are contained here. -* `CommandCustomIdCodec`: De/serializes commands names to/from customId strings, useful for creating message components that will trigger commands. -* `CommandResolver`: Resolves the command data that a given command interaction refers to. -* `CommandExecutor`: Executes command objects, checking its `CommandMiddlewareList` and passing any errors to its `ErrorHandler`. -* `CommandRepository`: Stores all the currently registered commands and their command application mappings. It's also responsible for registering commands at Discord. -* `CommandSubscriptionsContainer`: Stores the [๐Ÿ“ฉ Event Subscribers](../events/event-subscriber) that are subscribed to the [๐Ÿ‘‚ Client event bus](../events/event-manager.mdx#-client-event-bus) to listen for command interactions. -* `EventBus`: An [๐Ÿ“ฃ Event Bus](../events/event-bus) that emits command related events. +* [`๐Ÿ’ผ CommandManager`](./command-manager.mdx): The entry point for the command system, holding all the command-related objects and methods that use all of these objects. All objects below are contained here. +* [`๐Ÿ’ฌ CommandCustomIdCodec`](./command-customid-codec.mdx): De/serializes commands names to/from customId strings, useful for creating message components that will trigger commands. +* [`๐Ÿ”€ CommandResolver`](./command-resolver.mdx): Resolves the command that a given command interaction refers to. +* [`โšก CommandExecutor`](./command-executor.mdx): Executes commands, checking its `CommandMiddlewareList` and passing errors to the [`๐Ÿ’ซ ErrorHandler`](../../error/error-handling). +* [`๐Ÿ“” CommandRepository`](./command-repository.mdx): Stores all the currently registered commands. +* [`๐Ÿช CommandDeployer`](./command-deployer.mdx): Deploys commands to Discord and stores the ApplicationCommand mappings. +* [`๐Ÿ‘‚ CommandSubscriptionsContainer`](./command-subscribers.mdx): Stores the [๐Ÿ“ฉ Event Subscribers](../events/event-subscriber) that are subscribed to the [๐Ÿ‘‚ Client event bus](../events/event-manager.mdx#-client-event-bus) to listen for command interactions. +* [`๐Ÿ“ฃ EventBus`](./command-bus.mdx): An [๐Ÿ“ฃ Event Bus](../events/event-bus) that emits command related events. As well as the actual commands: -* `StandaloneCommand`: A fully executable, top level command with no children. Can be executed via a slash command, or -context menus (User or Message). -* `ParentCommand`: A non-executable command that only serves to store subcommands or subcommand groups. -* `SubCommand`: An executable command inside a `ParentCommand`. Can only be executed via a slash command. -* `SubCommandGroup`: An non-executable command inside a `ParentCommand` that only serves to store subcommands. - -:::tip -Commands don't depend on a specific bot, and you can reuse the same instance for many. - -From the `CommandExecutionMeta` passed on the command's execution (alongside the interaction) you can get: - -* The bot that received the command interaction, via `#getBot()`. -* The timestamp when the command execution began, via `#getCreatedAt()`. -* Other properties a filter or middleware may have set, via `#get()`. -::: +* [`๐Ÿš€ Standalone Command`](./commands/standalone-command.mdx): A slash command with no children. +* `๐Ÿงพ Context Menu Command`: A context menu command (either User or Message). +* [`๏ธ๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parent Command`](./commands/parent-command.mdx): A non-executable command that only serves to store subcommands or subcommand groups. +* [`๐Ÿงฉ SubCommand`](./commands/subcommand.mdx): An executable command inside a `ParentCommand`. Can only be executed via a slash command. +* [`๐Ÿ—‚๏ธ SubCommand Group`](./commands/subcommand-group.mdx): An non-executable command inside a `ParentCommand` that only serves to store subcommands.
Command handling process overview @@ -49,16 +40,16 @@ From the `CommandExecutionMeta` passed on the command's execution (alongside the Here's a high-level overview of how the system works, using a `ChatInputCommandInteraction` as an example: 1. A user triggers a command in Discord. - 2. The `'interactionCreate'` event is emitted by + 2. The `interactionCreate` event is emitted by the [Discord.js Client](https://discord.js.org/#/docs/discord.js/main/class/Client), passing the interaction as an argument. - 3. The `ClientEventBus` receives it and starts notifying its subscribers. - 4. One of these subscribers is the `CommandInteractionSubscriber`, stored on + 3. The [`ClientEventBus`](../events/event-overview#-subscribing-to-the-client) receives it and starts notifying its subscribers. + 4. One of these subscribers is the [`CommandInteractionSubscriber`](./command-subscribers), stored on the `CommandSubscriptionsContainer`. - 5. The subscriber passes the interaction to the `CommandManager`. - 6. The manager uses the `CommandResolver` to extract to what command the interaction refers to. - 7. The command reference data is passed to the `CommandRepository`, to search for a command that matches that data. - 8. If a command is found, the manager passes the execution to the `CommandExecutor`. + 5. The subscriber passes the interaction to the [`CommandManager`](./command-manager). + 6. The manager uses the [`CommandResolver`](./command-resolver) to extract to what command the interaction refers to, + passing the currently registered commands from the [`CommandRepository`](./command-repository). + 8. If a command is found, the manager passes the execution to the [`CommandExecutor`](./command-executor). 9. The executor checks what method it should execute according to the interaction. In this case, `ExecutableCommand#execute()`. 10. Once it knows what it should execute, it checks its `CommandMiddlewareList` which determines whether to @@ -78,34 +69,52 @@ participant "Client" as discordjs participant "ClientEventBus" as eventbus participant "CommandInteractionSubscriber" as subscriber participant "CommandManager" as manager -participant "CommandResolver" as resolver participant "CommandRepository" as repository +participant "CommandResolver" as resolver participant "CommandExecutor" as executor participant "EventBus" as eventbus2 discordjs -> eventbus : Emit 'interactionCreate' eventbus -> subscriber : Notify Subscribers subscriber -> manager : Pass Interaction -manager -> resolver : Extract reference data activate manager -activate resolver -resolver -> manager : Return reference data -deactivate resolver -manager -> repository : Search command +manager -> repository : Get all commands activate repository -repository -> manager : Return found command +repository -> manager deactivate repository -manager -> executor : Execute command +manager -> resolver : Search executable command +activate resolver +resolver -> manager +deactivate resolver +manager -> executor : Execute found command activate executor executor -> executor : Check Middleware executor -> executor : Execute command executor -> executor : Handle errors executor -> manager : Finish execution deactivate executor -manager -> eventbus2 : Emit event +manager --> eventbus2 : Emit event deactivate manager @enduml ``` +## ๐Ÿ“š Creating a command + +The overall process of creating a command is to instantiate and register it to a `CommandManager`. + +If the bot has started, it will immediately be deployed to Discord, and its `ApplicationCommand` mapping will be +available on the `CommandDeployer`. If the bot hasn't started, it will be queued to be deployed once it starts. + +Aditionally, commands don't depend on a specific bot, and you can reuse the same instance for many. + +:::warning +By default, commands are registered using `ApplicationCommandManager#set()`. This means that commands deployed to +Discord that are no longer registered will be deleted from there. +::: + +:::tip +Make sure to register all your commands before starting the bot to minimize the amount of API calls, since registering +them after the bot has started will cause another API call. +::: ## โœจ Quick examples @@ -114,15 +123,16 @@ deactivate manager ``` -1. Extend `AbstractStandaloneCommand`, implementing `execute()` and `data`. +1. Extend `AbstractStandaloneCommand`, implementing `execute()` and `createData()`. 2. Register it to a bot's `CommandManager`. ```ts class PingCommand extends AbstractStandaloneCommand { - protected data: StandaloneCommandData = { - name: 'ping', - description: 'Pong!', - }; + protected createData() { + return new SlashCommandBuilder() + .setName('ping') + .setDescription('Pong!'); + } public async execute(interaction: ChatInputCommandInteraction) { await interaction.reply('Pong!'); @@ -130,35 +140,99 @@ class PingCommand extends AbstractStandaloneCommand { } const command = new PingCommand(); -await bot.commands.addCommand(command); +await bot.commands.addCommands(command); ``` +```mdx-code-block + + +``` + +1. Extend `AbstractContextMenuCommand`, implementing `createData()`. +2. Implement `executeUser()` if you're making a User Context Menu, or `executeMessage()` if you're making a Message Context Menu. +3. Register it to a bot's `CommandManager`. + +* User Context Menu: + +```ts +class NameContextMenuCommand extends AbstractContextMenuCommand { + protected createData() { + return new ContextMenuCommandBuilder() + .setName('name') + .setType(ApplicationCommandType.User); + } + + public async executeUser(interaction: UserContextMenuCommandInteraction) { + const user = interaction.targetUser; + await interaction.reply(`This is ${user.username}`); + } +} + +const command = new NameContextMenuCommand(); +await bot.commands.addCommands(command); +``` + +* Message Context Menu: + +```ts +class MessageContentContextMenuCommand extends AbstractContextMenuCommand { + protected createData() { + return new ContextMenuCommandBuilder() + .setName('content') + .setType(ApplicationCommandType.Message); + } + + public async executeMessage(interaction: MessageContextMenuCommandInteraction) { + const message = interaction.targetMessage; + await interaction.reply(`This message's content is ${message.content}`); + } +} + +const command = new MessageContentContextMenuCommand(); +await bot.commands.addCommands(command); +``` + +:::danger +There won't be any type errors if you don't implement the correspondent execute method for your command (like if you +don't implement `executeUser()` with `ApplicationCommandType.User`), but you'll get a `NotImplementedError` once the +command is actually executed, which will be handled by the [`ErrorHandler`](../../error/error-handling). +::: + ```mdx-code-block ``` -1. Extend `AbstractParentCommand`, implementing `children` and `data`. This will be the command containing the subcommand. -2. Extend `AbstractSubCommand`, implementing `execute()` and `data`. +1. Extend `AbstractParentCommand`, implementing `children` and `createData()`. This will be the command containing the subcommand. +2. Extend `AbstractSubCommand`, implementing `execute()` and `createData()`. 3. Register the parent command to a bot's `CommandManager`. +:::danger +**Do not** add the subcommand as an option inside the `SlashCommandBuilder`. The serialization will do that for you, and +will actually throw an `AssertionError` if you do that. +::: + ```ts class UserParentCommand extends AbstractParentCommand { - protected data: ParentCommandData = { - name: 'user', - description: 'User-related commands', - }; + protected createData() { + return new SlashCommandBuilder() + .setName('user') + .setDescription('User-related commands'); + } - protected children: AbstractSubCommand[] = [ - new UserNameCommand(), + // highlight-start + protected children = [ + new NameSubCommand(this), ] + // highlight-end } -class UserNameCommand extends AbstractSubCommand { - protected data: SubCommandData = { - name: 'name', - description: 'Returns your name.', - }; +class NameSubCommand extends AbstractSubCommand { + protected createData() { + return new SlashCommandSubcommandBuilder() + .setName('name') + .setDescription('Returns your name.'); + } public async execute(interaction: ChatInputCommandInteraction) { await interaction.reply(interaction.user.username); @@ -166,41 +240,142 @@ class UserNameCommand extends AbstractSubCommand { } const parent = new UserParentCommand(); -await bot.commands.addCommand(parent); +await bot.commands.addCommands(parent); ``` +:::tip +Instead of implementing `children`, you could also add the subcommand with `#addChildren()`: +```ts +// Same classes as above but parent doesn't implement children: + +const parent = new UserParentCommand(); + +const subCommand = new NameSubCommand(group); +group.addChildren(subCommand); + +await bot.commands.addCommands(parent); +``` +::: ```mdx-code-block - + ``` -From your executable command (a `StandaloneCommand` or `SubCommand`) implement `autocomplete()`. This needs to -return an array of autocomplete options, either directly or on a promise (async method). +1. Extend `AbstractParentCommand`, implementing `children` and `createData()`. This will be the command containing the subcommand group. +2. Extend `AbstractSubCommandGroup`, implementing `createData()`. +3. Extend `AbstractSubCommand`, implementing `execute()` and `createData()`. +4. Register the parent command to a bot's `CommandManager`. + +:::danger +**Do not** add the subcommand group as an option inside the `SlashCommandBuilder` or the subcommand inside the +`SlashCommandSubcommandGroupBuilder`. The serialization will do that for you, and will actually throw an `AssertionError` +if you do that. +::: ```ts -class AutocompletableCommand extends AbstractStandaloneCommand { - public autocomplete( - option: AutocompleteFocusedOption, - interaction: AutocompleteInteraction, - ) { - return [ - { name: 'foo', value: 'bar' }, - ]; +// Create the ParentCommand +class PhotoParentCommand extends AbstractParentCommand { + protected createData() { + return new SlashCommandBuilder() + .setName('photo') + .setDescription('See photos of various things'); + } + + // highlight-start + protected children = [ + new AnimalPhotoSubCommandGroup(this), + ] +// highlight-end +} + +// Create the SubCommandGroup +class AnimalPhotoSubCommandGroup extends AbstractSubCommandGroup { + protected createData() { + return new SlashCommandSubcommandGroupBuilder() + .setName('animal') + .setDescription('See animal-related photos'); + } + + // highlight-start + protected children = [ + new DogSubCommand(this), + ] + // highlight-end +} + +// Create the SubCommand +class DogSubCommand extends AbstractSubCommand { + protected createData() { + return new SlashCommandSubcommandBuilder() + .setName('dog') + .setDescription('See a random photo of a dog.'); + } + + public async execute(interaction: ChatInputCommandInteraction) { + const photo = await getRandomDogPhoto(); + await interaction.reply(photo); } } + +const parent = new PhotoParentCommand(); +await bot.commands.addCommands(parent); ``` :::tip -The `CommandExecutor` can perform extra filtering logic to your autocomplete options before actually responding. If you -want to override this and directly answer with any given options, use the 4th parameter of `ExecutableCommand#autocomplete()`. -This is a callback that receives your options as a parameter. +Instead of implementing `children`, you could also add the group (and/or the subcommand) with `#addChildren()`: +```ts +// Same classes as above but parent and group don't implement children: + +const parent = new PhotoParentCommand(); + +// You can add the group first and then the subcommand or the way around, +// order doesn't matter. + +const group = new AnimalPhotoSubCommandGroup(parent); +parent.addChildren(group); + +const subCommand = new DogSubCommand(group); +group.addChildren(subCommand); -It's advised that you use this callback instead of `AutocompleteInteraction#respond()`, since the callback will perform -error routing logic if the method throws an error. +await bot.commands.addCommands(parent); +``` ::: +```mdx-code-block + + +``` + +From your executable command (a `StandaloneCommand` or `SubCommand`) implement `autocomplete()`. + +```ts +class AutocompletableCommand extends AbstractStandaloneCommand { + public async autocomplete(interaction: AutocompleteInteraction) { + await interaction.respond([ + { name: 'foo', value: 'bar' }, + ]); + } +} +``` + ```mdx-code-block ``` + +## ๐Ÿ”œ Next... + +Check the documentation of each command type: + +* [`๐Ÿš€ Standalone Command`](./commands/standalone-command.mdx): A slash command with no children. +* `๐Ÿ‘ค ContextMenu Command`: A context menu command (either User or Message). +* [`๏ธ๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parent Command`](./commands/parent-command.mdx): A non-executable command that only serves to store subcommands or subcommand groups. +* [`๐Ÿงฉ SubCommand`](./commands/subcommand.mdx): An executable command inside a `ParentCommand`. Can only be executed via a slash command. +* [`๐Ÿ—‚๏ธ SubCommand Group`](./commands/subcommand-group.mdx): An non-executable command inside a `ParentCommand` that only serves to store subcommands. + +Or check what you can do with commands: + +* Make components that call commands with the [`๐Ÿ’ฌ CommandCustomIdCodec`](./command-customid-codec.mdx). +* Type safely get command instances on the [`๐Ÿ“” CommandRepository`](./command-repository.mdx) +* Listen to command events on the [๐Ÿ“ฃ EventBus](./command-bus.mdx). diff --git a/apps/docs/docs/features/commands/command-repository.mdx b/apps/docs/docs/features/commands/command-repository.mdx index a8b62a00..36f2fbac 100644 --- a/apps/docs/docs/features/commands/command-repository.mdx +++ b/apps/docs/docs/features/commands/command-repository.mdx @@ -5,20 +5,13 @@ title: ๐Ÿ“” Command Repository import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -The `CommandRepository` is the object responsible for storing commands and their Discord.js `ApplicationCommand` -mappings, as well as adding commands at Discord using the `Client`. It's stored by a `CommandManager`, and you can get +The `CommandRepository` is the object responsible for storing commands. It's stored by a `CommandManager`, and you can get it via `CommandManager#getRepository()`. -:::danger -It's a pending rewrite to make the `CommandRepository` only store commands, and delegate the serialization and -registration of commands to a separate object. -::: - You can't modify the repository directly since the `CommandManager` returns a `ReadonlyCommandRepository` type, but the hidden methods are available at the `CommandManager`. This is because adding, removing and updating a command needs more logic than just modifying the repository, and the manager is responsible for coordinating this. - ## ๐Ÿ‘ท Creation You can create a command repository by either: @@ -32,8 +25,6 @@ You can create a command repository by either: ``` ```ts -import { DefaultCommandRepository } from '@nyx-discord/framework'; - class MyCommandRepository extends DefaultCommandRepository { // ... } @@ -51,8 +42,6 @@ const myBot = Bot.create((bot) => ({ ``` ```ts -import { CommandRepository } from '@nyx-discord/core'; - class MyCommandRepository implements CommandRepository { // ... } diff --git a/apps/docs/docs/features/commands/command-resolver.mdx b/apps/docs/docs/features/commands/command-resolver.mdx index 855a90b0..502c7af8 100644 --- a/apps/docs/docs/features/commands/command-resolver.mdx +++ b/apps/docs/docs/features/commands/command-resolver.mdx @@ -9,13 +9,6 @@ The `CommandResolver` is the object responsible for figuring to what command an `AutocompleteInteraction` is trying to call. It's stored by a `CommandManager`, and you can get it via `CommandManager#getResolver()`. -:::danger -A resolver does not locate the actual command. Instead, it returns a `CommandReferenceData` object that says data about -the command to find, like its name and type. The `CommandRepository` can then use this to actually locate the command. - -This intermediary is needed to avoid the resolver depending on the repository, and thus allowing you to reuse resolvers. -::: - ## ๐Ÿ‘ท Creation You can create a command resolver by either: @@ -29,8 +22,6 @@ You can create a command resolver by either: ``` ```ts -import { DefaultCommandResolver } from '@nyx-discord/framework'; - class MyCommandResolver extends DefaultCommandResolver { // ... } @@ -48,8 +39,6 @@ const myBot = Bot.create((bot) => ({ ``` ```ts -import { CommandResolver } from '@nyx-discord/core'; - class MyCommandResolver implements CommandResolver { // ... } diff --git a/apps/docs/docs/features/commands/command-subscribers.mdx b/apps/docs/docs/features/commands/command-subscribers.mdx index 35f8f90d..b8c5c0af 100644 --- a/apps/docs/docs/features/commands/command-subscribers.mdx +++ b/apps/docs/docs/features/commands/command-subscribers.mdx @@ -12,6 +12,9 @@ that are subscribed to the [Client Event Bus](../events/event-manager#-client-ev With this object you can override the manager's command subscribers, letting you add your own custom logic in runtime. +Specifically, it stores the `CommandInteractionSubscriber` and `DefaultCommandAutocompleteSubscriber`. +The first listens for command and component interactions, routing them to `CommandManager#execute()` and the second +listens for autocomplete interactions, routing them to `CommandManager#autocomplete()`. ## ๐Ÿ‘ท Creation @@ -26,8 +29,6 @@ You can create a command subscriptions container by either: ``` ```ts -import { DefaultCommandSubscriptionsContainer } from '@nyx-discord/framework'; - class MyCommandSubscriptionsContainer extends DefaultCommandSubscriptionsContainer { // ... } @@ -45,8 +46,6 @@ const myBot = Bot.create((bot) => ({ ``` ```ts -import { CommandSubscriptionsContainer } from '@nyx-discord/core'; - class MyCommandSubscriptionsContainer implements CommandSubscriptionsContainer { // ... } @@ -72,8 +71,6 @@ For more information about creating subscribers, check the [๐Ÿ“ฉ Event Subscribe ::: ```ts -import { AbstractDJSClientSubscriber } from 'discord.js'; - class MyCommandInteractionSubscriber extends AbstractDJSClientSubscriber<'interactionCreate'> { protected override readonly event = Events.InteractionCreate; @@ -93,6 +90,6 @@ await bot.commands.getSubscriptions().setInteractionSubscriber(subscriber); This will completely override the default subscriber. If your custom subscriber doesn't perform the logic that it should, your bot could ignore commands completely. -You can check the `DefaultCommandInteractionSubscriber` and `DefaultCommandAutocompleteSubscriber` to see how a +You can check the `DefaultCommandInteractionSubscriber` and `DefaultCommandAutocompleteSubscriber` source to see how a command subscriber should work. ::: diff --git a/apps/docs/docs/features/commands/commands/context-menu-command.mdx b/apps/docs/docs/features/commands/commands/context-menu-command.mdx new file mode 100644 index 00000000..2986260f --- /dev/null +++ b/apps/docs/docs/features/commands/commands/context-menu-command.mdx @@ -0,0 +1,66 @@ +--- +title: ๐Ÿงพ Context Menu Command +--- + +# ๐Ÿงพ Context Menu Command + +```mdx-code-block +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +``` + +Context menu commands are commands executed on special menus. Currently Discord has two types of context menus: + +* [๐Ÿ”— Message Context Menus](https://discord.com/developers/docs/interactions/application-commands#message-commands), shown when right clicking on a message. +* [๐Ÿ”— User Context Menus](.https://discord.com/developers/docs/interactions/application-commands#user-commands), shown when right clicking on a user. + +## ๐Ÿ‘ท Creation + +1. Create a context menu command class by either: + +* Extending `AbstractContextMenuCommand` from `@framework` (recommended). +* Implementing the `ContextMenuCommand` interface from `@core`. + +2. Implement `createData()` that returns the DJS `ContextMenuCommandBuilder` that stores the command's data, and: +* Implement `executeUser()` if the command is a user context menu. +* Implement `executeMessage()` if the command is a message context menu. + +3. Instantiate and register it to a bot's `CommandManager`. + +```mdx-code-block + + +``` + +```ts +class MyContextMenuCommand extends AbstractContextMenuCommand { + protected createData() { + return new ContextMenuCommandBuilder() + .setName('hello') + .setType(ApplicationCommandType.User); + } + + public async executeUser(interaction: UserContextMenuCommandInteraction) { + await interaction.reply('Hello!'); + } +} + +const command = new MyContextMenuCommand(); +await bot.commands.addCommands(command); +``` + +```mdx-code-block + + +``` + + +```ts +class MyContextMenuCommand implements ContextMenuCommand { + // ... +} + +const command = new MyContextMenuCommand(); +await bot.commands.addCommands(command); +``` + diff --git a/apps/docs/docs/features/commands/commands/parent-command.mdx b/apps/docs/docs/features/commands/commands/parent-command.mdx index 81471b88..74895928 100644 --- a/apps/docs/docs/features/commands/commands/parent-command.mdx +++ b/apps/docs/docs/features/commands/commands/parent-command.mdx @@ -9,28 +9,40 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ``` -Parent commands are non-executable, top level commands that only serve to store children. These children can be either -`SubCommands` or `SubCommandGroups`. +Parent commands are non-executable, top level commands that only serve to store [`๏ธ๐Ÿงฉ SubCommands`](./subcommand) or [`๐Ÿ—‚๏ธ SubCommand Group`](./subcommand-group). ## ๐Ÿ‘ท Creation -You can create a parent command by either: +1. Create a parent command class by either: * Extending `AbstractParentCommand` from `@framework` (recommended). * Implementing the `ParentCommand` interface from `@core`. +2. Implement `createData()` that returns the DJS `SlashCommandBuilder` that stores the command's data. + +:::danger +**Do not** add your children as options inside the `SlashCommandBuilder`. The serialization will do that for you, and +will actually throw an `AssertionError` if you do that. +::: + +2. Add subcommand or subcommand group children by either: + +* Specifying them in the `children` property. +* Adding them after instantiation with `ParentCommand#addChildren()`. + +3. Instantiate and register it to a bot's `CommandManager`. + ```mdx-code-block ``` ```ts -import { AbstractParentCommand } from '@nyx-discord/framework'; - class MyParentCommand extends AbstractParentCommand { - protected data: ParentCommandData = { - name: 'hello', - description: 'Hello world!', + protected createData() { + return new SlashCommandBuilder() + .setName('hello') + .setDescription('Hello world!'); } protected children = [ @@ -39,7 +51,7 @@ class MyParentCommand extends AbstractParentCommand { } const command = new MyParentCommand(); -await bot.commands.addCommand(command); +await bot.commands.addCommands(command); ``` ```mdx-code-block @@ -55,7 +67,7 @@ class MyParentCommand implements ParentCommand { } const command = new MyParentCommand(); -await bot.commands.addCommand(command); +await bot.commands.addCommands(command); ``` ```mdx-code-block @@ -66,14 +78,14 @@ await bot.commands.addCommand(command); ## ๐Ÿ”€ Manipulating children Normally, a parent's children are created alongside the parent, but they can also be added (and removed) on runtime with -`ParentCommand#addChild()`, `ParentCommand#removeChildByInstance()` and `ParentCommand#removeChildByName()`. +`ParentCommand#addChildren()`, `ParentCommand#removeChildByInstance()` and `ParentCommand#removeChildByName()`. After updating a parent command, you must call `CommandManager#update()` on it in order to reflect the changes on Discord. ```ts const parentCommand = new MyParentCommand(); -await this.bot.commands.addCommand(parentCommand); +await this.bot.commands.addCommands(parentCommand); // highlight-next-line // Adding a child @@ -84,7 +96,7 @@ const mySubCommand = new MySubCommand(parentCommand); // Throws IllegalDuplicateError if a child with that name already exists // Throws RangeError if no more children can be added to this parent command (25) // Throws AssertionError if this child's parent is not parentCommand -parentCommand.addChild(mySubCommand); +parentCommand.addChildren(mySubCommand); // warn-end await this.bot.commands.updateCommand(parentCommand); diff --git a/apps/docs/docs/features/commands/commands/standalone-command.mdx b/apps/docs/docs/features/commands/commands/standalone-command.mdx index fa614ca8..3866e87f 100644 --- a/apps/docs/docs/features/commands/commands/standalone-command.mdx +++ b/apps/docs/docs/features/commands/commands/standalone-command.mdx @@ -1,34 +1,37 @@ --- -title: ๏ธ๐Ÿ“ฃ Standalone Command +title: ๏ธ๐Ÿš€ Standalone Command --- -# ๏ธ๐Ÿ“ฃ Standalone Command +# ๏ธ๐Ÿš€ Standalone Command import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -Standalone commands are fully executable, top level commands that don't store any children. They can be executed via a -[Slash Command](#-chatinput-context-slash-commands), or context menus ([User](#-user-context) or [Message](#-message-context)). +Standalone commands are fully executable, top level commands that don't store any children. In Discord, they are +triggered on Slash Commands. ## ๐Ÿ‘ท Creation -You can create a standalone command by either: +1. Create a standalone command class by either: * Extending `AbstractStandaloneCommand` from `@framework` (recommended). * Implementing the `StandaloneCommand` interface from `@core`. +2. Implement `createData()` that returns the DJS `SlashCommandBuilder` that stores the command's data, and `execute()` that executes the command. + +3. Instantiate and register it to a bot's `CommandManager`. + ```mdx-code-block ``` ```ts -import { AbstractStandaloneCommand } from '@nyx-discord/framework'; - class MyStandaloneCommand extends AbstractStandaloneCommand { - protected data: StandaloneCommandData = { - name: 'hello', - description: 'Hello world!', + protected createData() { + return new SlashCommandBuilder() + .setName('hello') + .setDescription('Hello world!'); } public async execute( @@ -43,7 +46,7 @@ class MyStandaloneCommand extends AbstractStandaloneCommand { } const command = new MyStandaloneCommand(); -await bot.commands.addCommand(command); +await bot.commands.addCommands(command); ``` ```mdx-code-block @@ -52,106 +55,15 @@ await bot.commands.addCommand(command); ``` ```ts -import { StandaloneCommand } from '@nyx-discord/core'; - class MyStandaloneCommand implements StandaloneCommand { // ... } const command = new MyStandaloneCommand(); -await bot.commands.addCommand(command); +await bot.commands.addCommands(command); ``` ```mdx-code-block ``` - -## ๐Ÿงญ Standalone Command Contexts - -Standalone commands can be executed via a slash command, or context menus (User or Message). - -Which of these "contexts" a standalone command supports is stored inside a `StandaloneCommandContextData` object, -provided via `StandaloneCommand#getContexts()` (overriding `contexts` in `AbstractStandaloneCommand`). - -:::tip -By default, a command can only be executed via a slash command. That is, a context data of: - -```json -{ - "ChatInput": true, - "Message": false, - "User": false -} -``` -::: - -### ๐Ÿค– ChatInput context (Slash Commands) - -A standalone command can be executed via a slash command if `StandaloneCommand#getContexts().ChatInput` is `true`. After -that, when a `ChatInputCommandInteraction` is received, it will trigger `StandaloneCommand#execute()`. - -```ts -class ChatInputContextCommand extends AbstractStandaloneCommand { - protected contexts: StandaloneCommandContextData = { - // highlight-next-line - ChatInput: true, - Message: false, - User: false, - } - - public async execute(interaction: ChatInputCommandInteraction): Promise { - await interaction.reply('Pong!'); - } -} -``` - -### ๐Ÿ’ฌ Message context - -A standalone command can be executed via a slash command if `StandaloneCommand#getContexts().Message` is `true`. After -that, when a `MessageContextMenuCommandInteraction` is received, it will trigger `StandaloneCommand#executeMessage()`. - -```ts -class MessageContextCommand extends AbstractStandaloneCommand { - protected contexts: StandaloneCommandContextData = { - ChatInput: false, - // highlight-next-line - Message: true, - User: false, - } - - public async executeMessage(interaction: MessageContextMenuCommandInteraction): Promise { - await interaction.reply('Pong!'); - } -} -``` - -:::note -For more information, check -the [Discord Developer documentation on Message commands](https://discord.com/developers/docs/interactions/application-commands#message-commands). -::: - -### ๐Ÿ‘ค User context - -A standalone command can be executed via a slash command if `StandaloneCommand#getContexts().User` is `true`. After -that, when a `ChatInputCommandInteraction` is received, it will trigger `StandaloneCommand#executeUser()`. - -```ts -class UserContextCommand extends AbstractStandaloneCommand { - protected contexts: StandaloneCommandContextData = { - ChatInput: false, - Message: false, - // highlight-next-line - User: true, - } - - public async executeUser(interaction: UserContextMenuCommandInteraction): Promise { - await interaction.reply('Pong!'); - } -} -``` - -:::note -For more information, check -the [Discord Developer documentation on User commands](https://discord.com/developers/docs/interactions/application-commands#user-commands). -::: diff --git a/apps/docs/docs/features/commands/commands/subcommand-group.mdx b/apps/docs/docs/features/commands/commands/subcommand-group.mdx index 39773cbb..054b8752 100644 --- a/apps/docs/docs/features/commands/commands/subcommand-group.mdx +++ b/apps/docs/docs/features/commands/commands/subcommand-group.mdx @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; ``` Subcommand groups are non-executable commands inside [๏ธ๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parent Commands](./parent-command), whose only purpose is to -store subcommands. +store [`๏ธ๐Ÿงฉ SubCommands`](./subcommand). :::note While SubCommand Groups don't depend on a specific bot, they do depend on their `ParentCommand`. @@ -18,23 +18,34 @@ While SubCommand Groups don't depend on a specific bot, they do depend on their ## ๐Ÿ‘ท Creation -You can create a sub command group command by either: +1. Create a sub command group class by either: * Extending `AbstractSubCommandGroup` from `@framework` (recommended). * Implementing the `SubCommandGroup` interface from `@core`. +2. Implement `createData()` that returns the DJS `SlashCommandSubcommandGroupBuilder` that stores the group's data. + +:::danger +**Do not** add your children as options inside the `SlashCommandBuilder`. The serialization will do that for you, and +will actually throw an `AssertionError` if you do that. +::: + +2. Add subcommand children by either: + +* Specifying them in the `children` property. +* Adding them after instantiation with `SubCommandGroup#addChildren()`. + ```mdx-code-block ``` ```ts -import { AbstractSubCommandGroup } from '@nyx-discord/framework'; - class MySubCommandGroup extends AbstractSubCommandGroup { - protected data: SubCommandGroupData = { - name: 'hello', - description: 'Hello world!', + protected createData(): SlashCommandSubcommandGroupBuilder { + return new SlashCommandSubcommandGroupBuilder() + .setName('hello') + .setDescription('Hello world!'); } protected children = [ @@ -44,15 +55,15 @@ class MySubCommandGroup extends AbstractSubCommandGroup { // highlight-next-line // The parentCommand variable is the ParentCommand that will contain this group - +// highlight-next-line const subCommandGroup = new MySubCommandGroup(parentCommand); // warn-start // Throws IllegalDuplicateError if a child with that name already exists // Throws RangeError if no more children can be added to this command (25) // Throws AssertionError if this child's parent is not subCommandParent +parentCommand.addChildren(subCommandGroup); // warn-end -parentCommand.addChild(subCommandGroup); ``` ```mdx-code-block @@ -61,8 +72,6 @@ parentCommand.addChild(subCommandGroup); ``` ```ts -import { SubCommandGroup } from '@nyx-discord/core'; - class MySubCommandGroup implements SubCommandGroup { // ... } @@ -77,7 +86,7 @@ const subCommandGroup = new MySubCommandGroup(parentCommand); // Throws RangeError if no more children can be added to this command (25) // Throws AssertionError if this child's parent is not subCommandParent // warn-end -parentCommand.addChild(subCommandGroup); +parentCommand.addChildren(subCommandGroup); ``` ```mdx-code-block @@ -88,14 +97,14 @@ parentCommand.addChild(subCommandGroup); ## ๐Ÿ”€ Manipulating children Normally, a parent's children are created alongside the parent, but they can also be added (and removed) on runtime with -`SubCommandGroup#addChild()`, `SubCommandGroup#removeChildByInstance()` and `SubCommandGroup#removeChildByName()`. +`SubCommandGroup#addChildren()`, `SubCommandGroup#removeChildByInstance()` and `SubCommandGroup#removeChildByName()`. After updating a subcommand group, you must call `CommandManager#update()` on its parent in order to reflect the changes on Discord. ```ts const subCommandGroup = new MySubCommandGroup(parentCommand); -parentCommand.addChild(subCommandGroup); +parentCommand.addChildren(subCommandGroup); // highlight-next-line // Adding a subcommand @@ -107,7 +116,7 @@ const mySubcommand = new MySubCommand(subCommandGroup); // Throws RangeError if no more children can be added to this group (25) // Throws AssertionError if this subcommand's parent is not subCommandGroup // warn-end -subCommandGroup.addChild(mySubcommand); +subCommandGroup.addChildren(mySubcommand); await this.bot.commands.updateCommand(parentCommand); diff --git a/apps/docs/docs/features/commands/commands/subcommand.mdx b/apps/docs/docs/features/commands/commands/subcommand.mdx index 3d84cf5a..7816b44a 100644 --- a/apps/docs/docs/features/commands/commands/subcommand.mdx +++ b/apps/docs/docs/features/commands/commands/subcommand.mdx @@ -7,7 +7,7 @@ title: ๏ธ๐Ÿงฉ SubCommand import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -Subcommands are executable commands that are stored inside a `ParentCommand` or a `SubCommandGroup`. +Subcommands are executable commands that are stored inside a [`๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parent Command`](./parent-command) or a [`๐Ÿ—‚๏ธ SubCommand Group`](./subcommand-group). :::note While SubCommands don't depend on a specific bot, they do depend on their parent (`ParentCommand` or `SubCommandGroup`). @@ -15,39 +15,51 @@ While SubCommands don't depend on a specific bot, they do depend on their parent ## ๐Ÿ‘ท Creation -You can create a subcommand by either: +1. Create a subcommand class by either: * Extending `AbstractSubCommand` from `@framework` (recommended). * Implementing the `SubCommand` interface from `@core`. +2. Implement `createData()` that returns the DJS `SlashCommandSubcommandBuilder` that stores the subcommand's data, and `execute()` that executes the subcommand. + +3. Add it to a `ParentCommand` or `SubCommandGroup` with `#addChildren()`, or by overriding `children` on its fields. + ```mdx-code-block ``` ```ts -import { AbstractSubCommand } from '@nyx-discord/framework'; - class MySubCommand extends AbstractSubCommand { - protected data: SubCommandData = { - name: 'hello', - description: 'Hello world!', + protected createData() { + return new SlashCommandSubcommandBuilder() + .setName('hello') + .setDescription('Hello world!'); + } + + public async execute( + interaction: ChatInputCommandInteraction, + meta: CommandExecutionMeta + ) { + const bot = meta.getBot(); + const botId = bot.getId(); + + await interaction.reply(`Hello from Bot ${String(botId)} from MySubCommand!`); } } // highlight-start // The subCommandParent variable is the parent that will contain this subcommand, // whether a ParentCommand or a SubCommandGroup -// highlight-end - const subCommand = new MySubCommand(subCommandParent); +// highlight-end // warn-start // Throws IllegalDuplicateError if a child with that name already exists // Throws RangeError if no more children can be added to this command (25) // Throws AssertionError if this child's parent is not subCommandParent +subCommandParent.addChildren(subCommand); // warn-end -subCommandParent.addChild(subCommand); ``` ```mdx-code-block @@ -56,8 +68,6 @@ subCommandParent.addChild(subCommand); ``` ```ts -import { SubCommand } from '@nyx-discord/core'; - class MySubCommand implements SubCommand { // ... } @@ -65,16 +75,15 @@ class MySubCommand implements SubCommand { // highlight-start // The subCommandParent variable is the parent that will contain this subcommand, // whether a ParentCommand or a SubCommandGroup -// highlight-end - const subCommand = new MySubCommand(subCommandParent); +// highlight-end // warn-start // Throws IllegalDuplicateError if a child with that name already exists // Throws RangeError if no more children can be added to this command (25) // Throws AssertionError if this child's parent is not subCommandParent +subCommandParent.addChildren(subCommand); // warn-end -subCommandParent.addChild(subCommand); ``` ```mdx-code-block diff --git a/apps/docs/docs/features/events/event-bus.mdx b/apps/docs/docs/features/events/event-bus.mdx index 460793b6..d397f368 100644 --- a/apps/docs/docs/features/events/event-bus.mdx +++ b/apps/docs/docs/features/events/event-bus.mdx @@ -158,13 +158,6 @@ ticketBus .subscribe(invalidSubscriber); ``` -:::warning -A current limitation is that the bus (on `EventBus#subscribe()`) will only check that there's an event that matches the -given arguments. It will not check that the subscriber's event actually matches those types. - -It's advised that you create an abstract class for the subscriber you want, similar to the `AbstractDJSClientSubscriber`. -::: - ## ๐Ÿ”ƒ Subscriber Sorting By default, subscribers are sorted by their priority on `EventSubscriber#getPriority()`. You can override this by diff --git a/apps/docs/docs/features/events/event-overview.mdx b/apps/docs/docs/features/events/event-overview.mdx index 36f378a8..8ce73adc 100644 --- a/apps/docs/docs/features/events/event-overview.mdx +++ b/apps/docs/docs/features/events/event-overview.mdx @@ -386,10 +386,3 @@ ticketBus // error-next-line .subscribe(invalidSubscriber); ``` - -:::warning -A current limitation is that the bus (on `EventBus#subscribe()`) will only check that there's an event that matches the -given arguments. It will not check that the subscriber's event actually matches those types. - -It's advised that you create an abstract class for the subscriber you want, similar to the `AbstractDJSClientSubscriber`. -::: diff --git a/apps/docs/docs/intro.mdx b/apps/docs/docs/intro.mdx deleted file mode 100644 index 88c2aba0..00000000 --- a/apps/docs/docs/intro.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: ๐Ÿ“š Introduction ---- - -Hello there! ๐Ÿ‘‹ This guide is here to give you a quick overview on what nyx is and how it works. - -## ๐Ÿ“™ Description - -nyx is a framework to build Discord.js bots in Typescript. The main ideas behind nyx are: - -* It should be fully replaceable and extendable. Every working component can be replaced by your own given it fits the required interface. -* It should be bare-bones. `@core` has no runtime dependencies, and `@framework` has as little as possible. -* It should be straightforward to understand and setup. Hence, the existence of this documentation. - -Currently, nyx has five main features, each with its own dedicated section guide. These are: - -* [๐Ÿ’ป Commands](./features/commands/command-overview.mdx), for Discord application commands. -* [๐Ÿ“ธ Events](./features/events/event-overview.mdx), for subscribing to events, either from Discord.js or your own. -* [๏ธโฐ Schedules](./features/schedules/schedule-overview.mdx), for timed tasks. -* [๐Ÿ‘ค Sessions](./features/sessions/session-overview.mdx), for user interaction sessions. -* [๏ธ๐Ÿงฉ Plugins](./features/plugins/plugin-overview), for plugins that extend nyx. - -## ๐Ÿ“‹ Packages - -nyx is built with two packages: `@nyx-discord/core` and `@nyx-discord/framework`, abbreviated as `@core` and `@framework`. - -### ๐Ÿ’  Core - -* Defines all the object templates (interfaces) and their relations for nyx, acting as the blueprint. -* No runtime dependencies (since it doesn't have any actual code), only type dependencies which are `discord.js` and `@discordjs/collection`. -* It doesn't have any actual implementations, so it's not really usable by itself, without making you write all of them. - -### ๐Ÿงฑ Framework - -* Provides default, minimal implementations for `@core`, making it directly usable. -* Its implementations are designed to depend on interfaces from `@core`, rather than each other, making those dependencies fully replaceable by your own. -* Minimal runtime dependencies due to its "bare bones" ideology. - -### โ“ Which package to install - -* Most of the time, you'll want to use `@nyx-discord/framework`. Even if you want to include your own components, this library makes it easy to replace them via constructor injection. - -* If you want to develop something for nyx, such as a plugin, you can depend only on `@nyx-discord/core`. This way, you can depend on the interfaces and not any actual implementations, ensuring that your component will work across all nyx implementations. - -:::danger -These docs as a whole heavily assumes that you're using `@framework`'s implementations. The terms "by -default", "the base implementation", etc., all refer to these, as `@core` doesn't have any. -::: - diff --git a/apps/docs/docs/start.mdx b/apps/docs/docs/start.mdx index e61a6cb2..944a2cf0 100644 --- a/apps/docs/docs/start.mdx +++ b/apps/docs/docs/start.mdx @@ -1,5 +1,5 @@ --- -title: ๐Ÿš€ First Start +title: ๐Ÿš€ Start --- # ๐Ÿš€ First Start @@ -16,7 +16,7 @@ npm install @nyx-discord/framework @nyx-discord/core ## ๐Ÿ‘ท Bot Creation -The first step is to create a Discord.js client. This can be any client (as long as it's not logged in or destroyed). +The first coding step is to create a Discord.js client. This can be any client (as long as it's not logged in or destroyed). ```ts import { @@ -36,6 +36,8 @@ instance, and should return the options that you want for the bot. This is where you'll provide your bot's token, client instance, ID, logger, and other optional settings, like managers. +You can also create multiple bots and they'll all work independently of each other. + ```ts import { Bot } from '@nyx-discord/framework'; @@ -72,7 +74,7 @@ const myBot = new Bot((bot) => ({ ``` You will find this pattern in a lot of other `@framework` objects, where you can instantiate either with -`new SomeComponent(depA, depB, ...)` specifying every default dependency, or with `SomeComponent.create()` +`new SomeComponent(depA, depB, ...)` specifying every dependency, or with `SomeComponent.create()` "without" default dependencies. ```mdx-code-block @@ -88,15 +90,8 @@ know that its command manager is `MyCustomCommandManager` and not just the gener custom properties or methods that you may have added or modified. ::: -You can create multiple bots and they'll all work independently of each other. This -means you can have many bots for different purposes, in the same server. - ## ๐Ÿš€ Bot Starting -:::tip -You can read more information about bot status in its guide. -::: - To start a bot, you can set it up and then login, or just directly start it: ```ts @@ -108,7 +103,7 @@ await bot.login(); ``` :::danger -Trying to log in a bot without first setting it up will throw an `IllegalStateError`. +Trying to login a bot without first setting it up will throw an `IllegalStateError`. ::: As a quick explanation: @@ -140,10 +135,18 @@ const bot = Bot.create(() => ({ logger: console, })); +// Make sure you're on an async context to be able to await await bot.start(); ``` And that's it! You now have a running Discord bot with nyx. -If you're interested in the rest of nyx features, such as command adding, event subscribing, schedule creation, etc., -make sure to check the rest of the guides. +## ๐Ÿ”œ Next... + +Check the rest of nyx features like command adding, event subscribing, schedule creation and more: + +* [๐Ÿ’ป Commands](./features/commands/command-overview.mdx) - Create Discord application commands. +* [๐Ÿ“ธ Events](./features/events/event-overview.mdx) - Subscribe to events, either from Discord.js or your own. +* [๏ธโฐ Schedules](./features/schedules/schedule-overview.mdx) - Program timed tasks. +* [๐Ÿ‘ค Sessions](./features/sessions/session-overview.mdx) - Create user interaction sessions. +* [๏ธ๐Ÿงฉ Plugins](./features/plugins/plugin-overview) - Extend nyx. diff --git a/apps/docs/docs/welcome.mdx b/apps/docs/docs/welcome.mdx index d80fbd9b..24c01f4c 100644 --- a/apps/docs/docs/welcome.mdx +++ b/apps/docs/docs/welcome.mdx @@ -1,7 +1,6 @@ --- slug: / hide_title: true -hide_table_of_contents: true title: ๐Ÿ  Welcome --- @@ -13,11 +12,58 @@ title: ๐Ÿ  Welcome Hello there ๐Ÿ‘‹! Welcome to the Docusaurus documentation for nyx, a Discord.js Typescript bot framework. -Here, you'll discover how to create your own bot using nyx, how to customize existing and add new components to it. +Here, you'll discover how to create your own bot using nyx, how to customize the framework and add new components to it. :::danger nyx is not the best fit for beginners or simple bots. We assume you have some level of understanding of Typescript and Discord.js. +::: + +## ๐ŸŒ  Introduction + +nyx is a framework to build Discord.js bots in Typescript. The main ideas behind nyx are: + +* โ™ป **Fully replaceable and extendable**. Every working component can be replaced by your own given it fits the required interface. +* ๐Ÿงฑ **Bare-bones**. `@core` has no runtime dependencies, and `@framework` has as little as possible, which you can replace either way. +* ๐Ÿ˜€ **Easy to understand and setup**. Hacky or unnecessarily messy internal code is avoided, and documentation like this guide or [Typedoc](https://nyx-discord.github.io/nyx/typedoc/) is provided. +* ๐ŸŒ **DJS Native**. Nyx tries to reuse or pass DJS objects as much as possible to avoid falling behind on latest features or needing big rewrites when DJS changes. +* ๐Ÿ’ป **Developer friendly**. Type safety, predictability, error handling and extensibility are some features that make nyx easy to work with. + +Currently, nyx has five main features, each with its own dedicated section. These are: + +* [๐Ÿ’ป Commands](./features/commands/command-overview.mdx), for Discord application commands. +* [๐Ÿ“ธ Events](./features/events/event-overview.mdx), for subscribing to events, either from Discord.js or your own. +* [๏ธโฐ Schedules](./features/schedules/schedule-overview.mdx), for timed tasks. +* [๐Ÿ‘ค Sessions](./features/sessions/session-overview.mdx), for user interaction sessions. +* [๏ธ๐Ÿงฉ Plugins](./features/plugins/plugin-overview), for plugins that extend nyx. + +## ๐Ÿ“‹ Packages + +The framework is built with two packages: `@nyx-discord/core` and `@nyx-discord/framework`, abbreviated in the guide as `@core` and `@framework`. + +### ๐Ÿ’  Core -If you want something simpler, you might want to check out our bot template, `phanes`. +* Defines all the object templates (interfaces) and their relations for nyx, acting as the blueprint. +* No runtime dependencies (since it doesn't have any actual code), only type dependencies which are `discord.js` and `@discordjs/collection`. +* It doesn't have any actual implementations, so it's not really usable by itself. + +### ๐Ÿงฑ Framework + +* Provides default, minimal implementations for `@core`, making it directly usable. +* Its implementations are designed to depend on interfaces from `@core`, rather than each other, making those dependencies fully replaceable by your own. +* Minimal runtime dependencies due to its "bare bones" ideology. + +### โ“ Which package to install + +* Most of the time, you'll want to use `@framework`. Even if you want to include your own components, this library makes it easy to replace them via constructor injection. + +* If you want to develop something for nyx, such as a plugin, you can depend only on `@core`. This way, you can depend on the interfaces and not any actual implementations, ensuring that your component will work across all nyx implementations. + +:::danger +These docs as a whole heavily assumes that you're using `@framework`'s implementations. The terms "by +default", "the base implementation", etc., all refer to these, as `@core` doesn't have any. ::: + +## ๐Ÿ”œ Next... + +Check out the [๐Ÿš€ Start](./start.mdx) guide to get started on your first nyx bot! diff --git a/apps/docs/sidebars.ts b/apps/docs/sidebars.ts index dc74a3e9..55e3d279 100644 --- a/apps/docs/sidebars.ts +++ b/apps/docs/sidebars.ts @@ -27,7 +27,6 @@ const sidebars: SidebarsConfig = { // By default, Docusaurus generates a sidebar from the docs folder structure nyxSidebar: [ 'welcome', - 'intro', 'start', { @@ -61,9 +60,10 @@ const sidebars: SidebarsConfig = { items: [ { type: 'category', - label: '๐Ÿ’ป Command types', + label: '๐Ÿ“– Command types', items: [ 'features/commands/commands/standalone-command', + 'features/commands/commands/context-menu-command', 'features/commands/commands/parent-command', 'features/commands/commands/subcommand', 'features/commands/commands/subcommand-group', @@ -74,6 +74,7 @@ const sidebars: SidebarsConfig = { 'features/commands/command-customid-codec', 'features/commands/command-executor', 'features/commands/command-repository', + 'features/commands/command-deployer', 'features/commands/command-resolver', 'features/commands/command-subscribers', 'features/commands/command-bus', diff --git a/apps/docs/src/css/custom.css b/apps/docs/src/css/custom.css index a1e80256..d1c6717e 100644 --- a/apps/docs/src/css/custom.css +++ b/apps/docs/src/css/custom.css @@ -33,228 +33,226 @@ /* You can override the default Infima variables here. */ :root { - --ifm-code-font-size: 95%; - --ifm-font-family-base: "Roboto"; - --ifm-hr-border-color: #5662f6; - --ifm-font-size-base: 120%; - --ifm-footer-padding-vertical: 18px; + --ifm-font-family-base: "Roboto"; + --ifm-hr-border-color: #5662f6; + --ifm-footer-padding-vertical: 18px; - --ifm-code-padding-vertical: 0.2rem; - --ifm-code-padding-horizontal: 0.2rem; + --ifm-code-padding-vertical: 0.2rem; + --ifm-code-padding-horizontal: 0.2rem; - --docusaurus-error-code-line-bg: red; - --docusaurus-error-code-line-border: darkred; + --docusaurus-error-code-line-bg: red; + --docusaurus-error-code-line-border: darkred; } [data-theme='dark']:root { - --ifm-color-primary: #526bfc; - --ifm-color-primary-dark: #4653d7; - --ifm-color-primary-darker: #3c49c2; - --ifm-color-primary-darkest: #353da8; - --ifm-color-primary-light: #616dfd; - --ifm-color-primary-lighter: #7581ff; - --ifm-color-primary-lightest: #a3aaff; - --ifm-navbar-background-color: #242526; - - --docusaurus-highlighted-code-line-bg: #363636; - --docusaurus-error-code-line-bg: #482222; - --docusaurus-error-code-line-border: #bd0000; - --docusaurus-warn-code-line-bg: #2b2b20; - --docusaurus-warn-code-line-border: #bdba00; + --ifm-color-primary: #526bfc; + --ifm-color-primary-dark: #4653d7; + --ifm-color-primary-darker: #3c49c2; + --ifm-color-primary-darkest: #353da8; + --ifm-color-primary-light: #616dfd; + --ifm-color-primary-lighter: #7581ff; + --ifm-color-primary-lightest: #a3aaff; + --ifm-navbar-background-color: #242526; + + --docusaurus-highlighted-code-line-bg: #363636; + --docusaurus-error-code-line-bg: #482222; + --docusaurus-error-code-line-border: #bd0000; + --docusaurus-warn-code-line-bg: #2b2b20; + --docusaurus-warn-code-line-border: #bdba00; } [data-theme='light']:root { - --ifm-color-primary: #526bfc; - --ifm-color-primary-dark: #616dfd; - --ifm-color-primary-darker: #7581ff; - --ifm-color-primary-darkest: #a3aaff; - --ifm-color-primary-light: #4653d7; - --ifm-color-primary-lighter: #3c49c2; - --ifm-color-primary-lightest: #353da8; - --ifm-navbar-background-color: #e3e3e3; - --ifm-navbar-search-input-placeholder-color: #8a8b8d; - - --docusaurus-highlighted-code-line-bg: #ececec; - --docusaurus-error-code-line-bg: #f7d9db; - --docusaurus-error-code-line-border: #e55d5e; - --docusaurus-warn-code-line-bg: #ffffcf; - --docusaurus-warn-code-line-border: #e5de5d; + --ifm-color-primary: #526bfc; + --ifm-color-primary-dark: #616dfd; + --ifm-color-primary-darker: #7581ff; + --ifm-color-primary-darkest: #a3aaff; + --ifm-color-primary-light: #4653d7; + --ifm-color-primary-lighter: #3c49c2; + --ifm-color-primary-lightest: #353da8; + --ifm-navbar-background-color: #e3e3e3; + --ifm-navbar-search-input-placeholder-color: #8a8b8d; + + --docusaurus-highlighted-code-line-bg: #ececec; + --docusaurus-error-code-line-bg: #f7d9db; + --docusaurus-error-code-line-border: #e55d5e; + --docusaurus-warn-code-line-bg: #ffffcf; + --docusaurus-warn-code-line-border: #e5de5d; } /* Footer background color */ .footer { - background-color: var(--ifm-color-emphasis-100); - color: var(--ifm-color-emphasis-1000); + background-color: var(--ifm-color-emphasis-100); + color: var(--ifm-color-emphasis-1000); } .docusaurus-highlight-code-line { - background-color: rgb(72, 77, 91); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); + background-color: rgb(72, 77, 91); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); } .code-block-error-line { - background-color: var(--docusaurus-error-code-line-bg); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); - border-left: 3px solid var(--docusaurus-error-code-line-border); + background-color: var(--docusaurus-error-code-line-bg); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); + border-left: 3px solid var(--docusaurus-error-code-line-border); } .code-block-warn-line { - background-color: var(--docusaurus-warn-code-line-bg); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); - border-left: 3px solid var(--docusaurus-warn-code-line-border); + background-color: var(--docusaurus-warn-code-line-bg); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); + border-left: 3px solid var(--docusaurus-warn-code-line-border); } html[data-theme="dark"] { - --ifm-color-emphasis-0: var(--ifm-color-gray-1000); - --ifm-color-emphasis-100: var(--ifm-color-gray-900); - --ifm-color-emphasis-200: var(--ifm-color-gray-800); - --ifm-color-emphasis-300: var(--ifm-color-gray-700); - --ifm-color-emphasis-400: var(--ifm-color-gray-600); - --ifm-color-emphasis-500: var(--ifm-color-gray-500); - --ifm-color-emphasis-600: var(--ifm-color-gray-400); - --ifm-color-emphasis-700: var(--ifm-color-gray-300); - --ifm-color-emphasis-800: var(--ifm-color-gray-200); - --ifm-color-emphasis-900: var(--ifm-color-gray-100); - --ifm-color-emphasis-1000: var(--ifm-color-gray-0); - --ifm-background-color: #181a1b; - --ifm-hover-overlay: rgba(255, 255, 255, 0.05); - --ifm-menu-link-sublist-icon-filter: invert(100%) sepia(94%) saturate(17%) hue-rotate(223deg) brightness(104%) contrast(98%); - --ifm-color-content-secondary: rgba(255, 255, 255, 1); - --ifm-breadcrumb-separator-filter: invert(64%) sepia(11%) saturate(0%) hue-rotate(149deg) brightness(99%) contrast(95%); + --ifm-color-emphasis-0: var(--ifm-color-gray-1000); + --ifm-color-emphasis-100: var(--ifm-color-gray-900); + --ifm-color-emphasis-200: var(--ifm-color-gray-800); + --ifm-color-emphasis-300: var(--ifm-color-gray-700); + --ifm-color-emphasis-400: var(--ifm-color-gray-600); + --ifm-color-emphasis-500: var(--ifm-color-gray-500); + --ifm-color-emphasis-600: var(--ifm-color-gray-400); + --ifm-color-emphasis-700: var(--ifm-color-gray-300); + --ifm-color-emphasis-800: var(--ifm-color-gray-200); + --ifm-color-emphasis-900: var(--ifm-color-gray-100); + --ifm-color-emphasis-1000: var(--ifm-color-gray-0); + --ifm-background-color: #181a1b; + --ifm-hover-overlay: rgba(255, 255, 255, 0.05); + --ifm-menu-link-sublist-icon-filter: invert(100%) sepia(94%) saturate(17%) hue-rotate(223deg) brightness(104%) contrast(98%); + --ifm-color-content-secondary: rgba(255, 255, 255, 1); + --ifm-breadcrumb-separator-filter: invert(64%) sepia(11%) saturate(0%) hue-rotate(149deg) brightness(99%) contrast(95%); } html[data-theme="light"] { - /* Because #FFFFFF is just pure madness */ - --ifm-background-color: #f8f8f8; - --ifm-menu-color: #5b5b5b; + /* Because #FFFFFF is just pure madness */ + --ifm-background-color: #f8f8f8; + --ifm-menu-color: #5b5b5b; } .table-container { - display: block; - overflow-x: auto; - margin-top: 5px; - margin-bottom: 10px; - box-shadow: var(--ifm-global-shadow-lw) !important; - border-radius: 8px; + display: block; + overflow-x: auto; + margin-top: 5px; + margin-bottom: 10px; + box-shadow: var(--ifm-global-shadow-lw) !important; + border-radius: 8px; } table { - display: table; - width: 100%; - overflow-wrap: break-word; - border-collapse: collapse; - border-style: hidden; - margin-bottom: 0px; + display: table; + width: 100%; + overflow-wrap: break-word; + border-collapse: collapse; + border-style: hidden; + margin-bottom: 0px; } table span { - margin-right: 5px; + margin-right: 5px; } table tr { - padding-top: 3px; - padding-bottom: 3px; + padding-top: 3px; + padding-bottom: 3px; } table th { - background-color: rgb(72, 77, 91, 0.1); - padding: 8px; + background-color: rgb(72, 77, 91, 0.1); + padding: 8px; } table tbody tr th { - padding: 3px; + padding: 3px; } table td { - padding-top: 5px; - padding-bottom: 5px; - background-color: rgb(72, 77, 91, 0); + padding-top: 5px; + padding-bottom: 5px; + background-color: rgb(72, 77, 91, 0); } table tr:nth-child(even) td { - border-color: rgb(72, 77, 91, 0.1); + border-color: rgb(72, 77, 91, 0.1); } table tr:nth-child(odd) td { - border-color: rgb(72, 77, 91, 0.1); + border-color: rgb(72, 77, 91, 0.1); } table label { - display: block; - unicode-bidi: embed; - font-family: monospace; - font-weight: 600; - font-size: larger; - border-radius: 4px; - padding: 1px 4px; + display: block; + unicode-bidi: embed; + font-family: monospace; + font-weight: 600; + font-size: larger; + border-radius: 4px; + padding: 1px 4px; } [data-theme='dark'] table label { - color: #e07683; + color: #e07683; } [data-theme='light'] table label { - color: #80004b; + color: #80004b; } .menu__link--sublist { - font-size: 18px; + font-size: 18px; } .menu__list { - font-size: 14px; + font-size: 14px; } [data-theme='light'] .navbar__search-input::placeholder { - color: #1b1b1b; + color: #1b1b1b; } [data-theme='dark'] aside { - background-color: #212121; + background-color: #212121; } [data-theme='light'] aside { - background-color: #f3f3f3; + background-color: #f3f3f3; } label b, label strong { - color: var(--ifm-color-emphasis-1000) !important; + color: var(--ifm-color-emphasis-1000) !important; } .markdown a:not(.hash-link, .card) { - color: var(--ifm-color-primary-lightest); - text-decoration: underline; - font-weight: 450; + color: var(--ifm-color-primary-lightest); + text-decoration: underline; + font-weight: 450; } [data-theme='dark'] .admonition-note { - background-color: #2b2b2b; - border-color: var(--ifm-color-primary-light); - color: rgb(233, 233, 233); + background-color: #2b2b2b; + border-color: var(--ifm-color-primary-light); + color: rgb(233, 233, 233); } [data-theme='light'] .admonition-note { - background-color: #f0f0f0; - border-color: var(--ifm-color-primary-light); + background-color: #f0f0f0; + border-color: var(--ifm-color-primary-light); } blockquote { - line-height: 1.5; - font-size: 100%; + line-height: 1.5; + font-size: 100%; } .float-right { - margin-left: 5%; - float: right; + margin-left: 5%; + float: right; } [data-theme='light'] .kroki-image { - -webkit-filter: invert(1); - filter: invert(1); + -webkit-filter: invert(1); + filter: invert(1); } diff --git a/packages/core/src/bot/BotOptions.ts b/packages/core/src/bot/BotOptions.ts index 8eeb032b..a3669693 100644 --- a/packages/core/src/bot/BotOptions.ts +++ b/packages/core/src/bot/BotOptions.ts @@ -46,7 +46,6 @@ export interface BotOptions< token: string; client: Client; id: Identifier; - refreshCommands: boolean; logger: ConcreteLogger; commands: ConcreteCommandManager; diff --git a/packages/core/src/customId/CustomIdCodec.ts b/packages/core/src/customId/CustomIdCodec.ts index 94eb7cd1..57a6d8e6 100644 --- a/packages/core/src/customId/CustomIdCodec.ts +++ b/packages/core/src/customId/CustomIdCodec.ts @@ -22,12 +22,11 @@ * SOFTWARE. */ -import type { Identifiable } from '../identity/Identifiable.js'; import type { StringIterator } from '../string/StringIterator'; import type { CustomIdBuilder } from './CustomIdBuilder'; /** An object responsible for creating and manipulating customIds that refer to objects. */ -export interface CustomIdCodec> { +export interface CustomIdCodec { /** Creates a customId that refers to the passed object. */ serializeToCustomId(object: Serialized): string; diff --git a/packages/core/src/features/command/CommandManager.ts b/packages/core/src/features/command/CommandManager.ts index 89c34b58..3532067a 100644 --- a/packages/core/src/features/command/CommandManager.ts +++ b/packages/core/src/features/command/CommandManager.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,16 +28,16 @@ import type { BotAware } from '../../bot/BotAware.js'; import type { BotLifecycleObserver } from '../../types/BotLifecycleObserver'; import type { EventBus } from '../event/bus/EventBus.js'; import type { EventSubscriber } from '../event/subscriber/EventSubscriber.js'; -import type { ParentCommand } from './commands/ParentCommand'; import type { TopLevelCommand } from './commands/TopLevelCommand.js'; import type { CommandCustomIdCodec } from './customId/CommandCustomIdCodec.js'; +import type { ReadonlyCommandDeployer } from './deploy/ReadonlyCommandDeployer'; import type { CommandSubscriptionsContainer } from './event/CommandSubscriptionsContainer.js'; import type { CommandEventArgs } from './events/CommandEvent.js'; import type { CommandExecutor } from './execution/executor/CommandExecutor.js'; import type { CommandExecutionMeta } from './execution/meta/CommandExecutionMeta.js'; import type { CommandExecutableInteraction } from './interaction/CommandExecutableInteraction.js'; import type { ReadonlyCommandRepository } from './repository/ReadonlyCommandRepository.js'; -import type { CommandResolver } from './resolver/CommandResolver.js'; +import type { CommandResolver } from './resolve/CommandResolver'; /** An object that holds methods for interacting with the bot's {@link Command commands}. */ export interface CommandManager extends BotAware, BotLifecycleObserver { @@ -64,12 +64,12 @@ export interface CommandManager extends BotAware, BotLifecycleObserver { ): Awaitable; /** - * Adds a {@link TopLevelCommand command} and registers it on Discord, if the + * Adds {@link TopLevelCommand commands} and registers them on Discord, if the * bot has started. * * @throws {IllegalDuplicateError} If a command with that same data exists. */ - addCommand(command: TopLevelCommand): Awaitable; + addCommands(...commands: TopLevelCommand[]): Awaitable; /** * Removes a {@link TopLevelCommand command} and unregisters it from Discord, @@ -77,10 +77,16 @@ export interface CommandManager extends BotAware, BotLifecycleObserver { * * @throws {ObjectNotFoundError} If that command is not registered. */ - removeCommand(command: TopLevelCommand): Awaitable; + removeCommands(...commands: TopLevelCommand[]): Awaitable; - /** Updates a parent command on Discord, after its children have changed. */ - updateParentCommand(command: ParentCommand): Awaitable; + /** + * Edits commands on Discord. Requires the bot to have started. + * + * @throws {IllegalStateError} If the bot has not started. + * @throws {ObjectNotFoundError} If that command is not deployed. + * @throws {Error} If there was an error while editing the commands. + */ + editCommands(...commands: TopLevelCommand[]): Awaitable; /** * Subscribes an event subscriber to the manager's bus. @@ -88,7 +94,7 @@ export interface CommandManager extends BotAware, BotLifecycleObserver { * Alias of: * ``` * const managerBus = commandManager.getEventBus(); - * await eventManager.subscribe(subscriber, managerBus); + * await managerBus.subscribe(subscriber); * ``` */ subscribe( @@ -112,4 +118,7 @@ export interface CommandManager extends BotAware, BotLifecycleObserver { /** Returns the {@link EventBus} for this manager. */ getEventBus(): EventBus; + + /** Returns the {@link CommandDeployer} for this manager. */ + getDeployer(): ReadonlyCommandDeployer; } diff --git a/packages/core/src/features/command/commands/abstract/Command.ts b/packages/core/src/features/command/commands/Command.ts similarity index 50% rename from packages/core/src/features/command/commands/abstract/Command.ts rename to packages/core/src/features/command/commands/Command.ts index 96e6aa17..93356123 100644 --- a/packages/core/src/features/command/commands/abstract/Command.ts +++ b/packages/core/src/features/command/commands/Command.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,37 +22,29 @@ * SOFTWARE. */ -import type { Identifiable } from '../../../../identity/Identifiable.js'; -import type { Metadatable } from '../../../../meta/Metadatable.js'; -import type { CommandData } from '../../data/command/CommandData.js'; -import type { CommandVisitor } from '../../visitor/CommandVisitor.js'; -import type { ParentCommand } from '../ParentCommand.js'; -import type { StandaloneCommand } from '../StandaloneCommand.js'; -import type { SubCommand } from '../SubCommand.js'; -import type { SubCommandGroup } from '../SubCommandGroup.js'; -import type { ExecutableCommand } from './ExecutableCommand.js'; +import type { Metadatable } from '../../../meta/Metadatable'; +import type { ContextMenuCommand } from './ContextMenuCommand'; +import type { ParentCommand } from './ParentCommand'; +import type { StandaloneCommand } from './StandaloneCommand'; +import type { SubCommand } from './SubCommand'; +import type { SubCommandGroup } from './SubCommandGroup'; /** The base of every command that can be executed or stored by the bot. */ -export interface Command - extends Identifiable, - Metadatable { - /** Accepts a CommandVisitor. */ - acceptVisitor(visitor: CommandVisitor): void; +export interface Command extends Metadatable { + /** Returns the data for this command. */ + getData(): Data; - /** Returns the data of this command. */ - getData(): Readonly; - - /** Returns a readable name of this command, usually the command's name and parents. */ - getReadableName(): string; - - /** Returns whether this is an ExecutableCommand. */ - isExecutable(): this is ExecutableCommand; + /** Returns the name of this command. */ + getName(): string; /** Returns whether this is a ParentCommand. */ - isParentCommand(): this is ParentCommand; + isParent(): this is ParentCommand; + + /** Returns whether this is an ContextMenuCommand. */ + isContextMenu(): this is ContextMenuCommand; - /** Returns whether this is an StandaloneCommand. */ - isStandaloneCommand(): this is StandaloneCommand; + /** Returns whether this is a StandaloneCommand. */ + isStandalone(): this is StandaloneCommand; /** Returns whether this is a SubCommand. */ isSubCommand(): this is SubCommand; @@ -60,6 +52,6 @@ export interface Command /** Returns whether this is a SubCommandGroup. */ isSubCommandGroup(): this is SubCommandGroup; - /** Returns a string representation of the command. Should be alias for {@link getReadableName}. */ - toString(): string; + /** Returns the names of the tree of this command. */ + getNameTree(): ReadonlyArray; } diff --git a/packages/core/src/features/command/data/command/TopLevelCommandData.ts b/packages/core/src/features/command/commands/ContextMenuCommand.ts similarity index 59% rename from packages/core/src/features/command/data/command/TopLevelCommandData.ts rename to packages/core/src/features/command/commands/ContextMenuCommand.ts index 6bf5efdc..174a884f 100644 --- a/packages/core/src/features/command/data/command/TopLevelCommandData.ts +++ b/packages/core/src/features/command/commands/ContextMenuCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,17 +22,22 @@ * SOFTWARE. */ -import type { PermissionResolvable } from 'discord.js'; +import type { + ContextMenuCommandBuilder, + MessageContextMenuCommandInteraction, + Snowflake, + UserContextMenuCommandInteraction, +} from 'discord.js'; +import type { Identifiable } from '../../../identity/Identifiable'; +import type { ExecutableCommand } from './executable/ExecutableCommand'; -import type { CommandData } from './CommandData.js'; - -/** The command data for a top level command. */ -export interface TopLevelCommandData extends CommandData { - /** Whether the command is enabled in DMs. */ - dmPermission?: boolean; - /** - * The bitfield used to determine the default permissions a member needs in order to run the - * command. See {@link BaseApplicationCommandData#defaultMemberPermissions}. - */ - defaultMemberPermissions?: PermissionResolvable | null; +/** A command that can be executed by a context menu. */ +export interface ContextMenuCommand + extends ExecutableCommand< + ReturnType, + MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction + >, + Identifiable { + /** Gets the guilds this command can be executed in. `null` for global commands. */ + getGuilds(): ReadonlyArray | null; } diff --git a/packages/core/src/features/command/commands/ParentCommand.ts b/packages/core/src/features/command/commands/ParentCommand.ts index c4972e91..b917322c 100644 --- a/packages/core/src/features/command/commands/ParentCommand.ts +++ b/packages/core/src/features/command/commands/ParentCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,10 @@ * SOFTWARE. */ -import type { ParentCommandData } from '../data/command/ParentCommandData.js'; -import type { ChildableCommand } from './abstract/ChildableCommand.js'; +import type { SlashCommandSubcommandsOnlyBuilder, Snowflake } from 'discord.js'; + +import type { Identifiable } from '../../../identity/Identifiable'; +import type { ChildableCommand } from './child/ChildableCommand'; import type { SubCommand } from './SubCommand.js'; import type { SubCommandGroup } from './SubCommandGroup.js'; @@ -33,4 +35,11 @@ import type { SubCommandGroup } from './SubCommandGroup.js'; * cannot be executed by themselves, a subcommand must be specified. */ export interface ParentCommand - extends ChildableCommand {} + extends ChildableCommand< + ReturnType, + SubCommand | SubCommandGroup + >, + Identifiable { + /** Gets the guilds this command can be executed in. `null` for global commands. */ + getGuilds(): ReadonlyArray | null; +} diff --git a/packages/core/src/features/command/commands/StandaloneCommand.ts b/packages/core/src/features/command/commands/StandaloneCommand.ts index 97942329..b5ee465d 100644 --- a/packages/core/src/features/command/commands/StandaloneCommand.ts +++ b/packages/core/src/features/command/commands/StandaloneCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,36 +22,17 @@ * SOFTWARE. */ -import type { - Awaitable, - MessageContextMenuCommandInteraction, - UserContextMenuCommandInteraction, -} from 'discord.js'; -import type { ApplicationCommandType } from 'discord.js'; -import type { StandaloneCommandData } from '../data/command/StandaloneCommandData.js'; -import type { CommandExecutionMeta } from '../execution/meta/CommandExecutionMeta.js'; -import type { ExecutableCommand } from './abstract/ExecutableCommand.js'; +import type { SlashCommandOptionsOnlyBuilder, Snowflake } from 'discord.js'; -/** - * A fully executable top level command. - * - * Can include whether to support - * {@link https://discord.com/developers/docs/interactions/application-commands#user-commands User Context Menus} and {@link https://discord.com/developers/docs/interactions/application-commands#message-commands Message Context Menus} as well, or even to only support them. - */ -export interface StandaloneCommand - extends ExecutableCommand { - /** Returns the {@link StandaloneCommandContextData contexts} that the command supports. */ - getContexts(): ReadonlyArray; +import type { Identifiable } from '../../../identity/Identifiable'; +import type { ChatExecutableCommand } from './executable/ChatExecutableCommand'; - /** Execute a user context menu interaction. */ - executeUser( - interaction: UserContextMenuCommandInteraction, - metadata: CommandExecutionMeta, - ): Awaitable; - - /** Execute a message context menu interaction. */ - executeMessage( - interaction: MessageContextMenuCommandInteraction, - metadata: CommandExecutionMeta, - ): Awaitable; +/** A standalone command, i.e. a slash command with no children. */ +export interface StandaloneCommand + extends ChatExecutableCommand< + ReturnType + >, + Identifiable { + /** Gets the guilds this command can be executed in. `null` for global commands. */ + getGuilds(): ReadonlyArray | null; } diff --git a/packages/core/src/features/command/commands/SubCommand.ts b/packages/core/src/features/command/commands/SubCommand.ts index 6af9ea4d..3479a8ac 100644 --- a/packages/core/src/features/command/commands/SubCommand.ts +++ b/packages/core/src/features/command/commands/SubCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,13 +22,16 @@ * SOFTWARE. */ -import type { SubCommandData } from '../data/command/SubCommandData.js'; -import type { ChildCommand } from './abstract/ChildCommand.js'; -import type { ExecutableCommand } from './abstract/ExecutableCommand.js'; +import type { SlashCommandSubcommandBuilder } from 'discord.js'; +import type { ChildCommand } from './child/ChildCommand'; +import type { ChatExecutableCommand } from './executable/ChatExecutableCommand'; import type { ParentCommand } from './ParentCommand.js'; import type { SubCommandGroup } from './SubCommandGroup.js'; /** A child, executable command that belongs to a {@link ParentCommand} or {@link SubCommandGroup}. */ export interface SubCommand - extends ExecutableCommand, - ChildCommand {} + extends ChatExecutableCommand, + ChildCommand< + SlashCommandSubcommandBuilder, + ParentCommand | SubCommandGroup + > {} diff --git a/packages/core/src/features/command/commands/SubCommandGroup.ts b/packages/core/src/features/command/commands/SubCommandGroup.ts index cd32b2b0..39ea0276 100644 --- a/packages/core/src/features/command/commands/SubCommandGroup.ts +++ b/packages/core/src/features/command/commands/SubCommandGroup.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,9 +22,9 @@ * SOFTWARE. */ -import type { SubCommandGroupData } from '../data/command/SubCommandGroupData.js'; -import type { ChildableCommand } from './abstract/ChildableCommand.js'; -import type { ChildCommand } from './abstract/ChildCommand.js'; +import type { SlashCommandSubcommandGroupBuilder } from 'discord.js'; +import type { ChildableCommand } from './child/ChildableCommand'; +import type { ChildCommand } from './child/ChildCommand'; import type { ParentCommand } from './ParentCommand.js'; import type { SubCommand } from './SubCommand.js'; @@ -33,5 +33,5 @@ import type { SubCommand } from './SubCommand.js'; * This cannot be executed by itself and merely exists for grouping {@link SubCommand subcommands}. */ export interface SubCommandGroup - extends ChildableCommand, - ChildCommand {} + extends ChildableCommand, + ChildCommand {} diff --git a/packages/core/src/features/command/commands/TopLevelCommand.ts b/packages/core/src/features/command/commands/TopLevelCommand.ts index eb9983bf..9469df90 100644 --- a/packages/core/src/features/command/commands/TopLevelCommand.ts +++ b/packages/core/src/features/command/commands/TopLevelCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,12 @@ * SOFTWARE. */ +import type { ContextMenuCommand } from './ContextMenuCommand'; import type { ParentCommand } from './ParentCommand.js'; -import type { StandaloneCommand } from './StandaloneCommand.js'; +import type { StandaloneCommand } from './StandaloneCommand'; /** Type of supported top level commands. */ -export type TopLevelCommand = ParentCommand | StandaloneCommand; +export type TopLevelCommand = + | ParentCommand + | StandaloneCommand + | ContextMenuCommand; diff --git a/packages/core/src/features/command/commands/abstract/ExecutableCommand.ts b/packages/core/src/features/command/commands/abstract/ExecutableCommand.ts deleted file mode 100644 index 47332d2e..00000000 --- a/packages/core/src/features/command/commands/abstract/ExecutableCommand.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Amgelo563 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import type { - ApplicationCommandOptionChoiceData, - AutocompleteFocusedOption, - AutocompleteInteraction, - Awaitable, - ChatInputCommandInteraction, -} from 'discord.js'; -import type { Filterable } from '../../../../filter/Filterable.js'; -import type { CommandData } from '../../data/command/CommandData.js'; -import type { CommandOptionData } from '../../data/option/CommandOptionData.js'; -import type { CommandExecutionMeta } from '../../execution/meta/CommandExecutionMeta.js'; -import type { CommandFilter } from '../../filter/CommandFilter.js'; -import type { ComponentCommandInteraction } from '../../interaction/ComponentCommandInteraction.js'; -import type { CommandReferenceData } from '../../resolver/CommandReferenceData'; -import type { Command } from './Command.js'; - -/** A command that can be executed (called) by an interaction. */ -export interface ExecutableCommand - extends Command, - Filterable> { - /** Executes this command from a {@link ChatInputCommandInteraction}. */ - execute( - interaction: ChatInputCommandInteraction, - metadata: CommandExecutionMeta, - ): Awaitable; - - /** Returns this command's options (arguments). */ - getOptions(): ReadonlyArray; - - /** Returns the autocomplete options for the given interaction. */ - autocomplete( - option: AutocompleteFocusedOption, - interaction: AutocompleteInteraction, - metadata: CommandExecutionMeta, - respond: (options: ApplicationCommandOptionChoiceData[]) => Awaitable, - ): Awaitable>; - - /** Handle a Interaction whose customId refers to this object. */ - handleInteraction( - _interaction: ComponentCommandInteraction, - metadata: CommandExecutionMeta, - ): Awaitable; - - /** Returns the reference data of this command. */ - toReferenceData(): CommandReferenceData; -} diff --git a/packages/core/src/features/command/commands/abstract/ChildCommand.ts b/packages/core/src/features/command/commands/child/ChildCommand.ts similarity index 82% rename from packages/core/src/features/command/commands/abstract/ChildCommand.ts rename to packages/core/src/features/command/commands/child/ChildCommand.ts index fa2a41a0..1aec951c 100644 --- a/packages/core/src/features/command/commands/abstract/ChildCommand.ts +++ b/packages/core/src/features/command/commands/child/ChildCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,15 +22,14 @@ * SOFTWARE. */ -import type { CommandData } from '../../data/command/CommandData.js'; -import type { Command } from './Command.js'; -import type { ChildableCommand } from './ChildableCommand.js'; +import type { Command } from '../Command'; +import type { ChildableCommand } from './ChildableCommand'; /** A command that belongs to a {@link ChildableCommand}. */ export interface ChildCommand< - Data extends CommandData, + Data, // eslint-disable-next-line @typescript-eslint/no-explicit-any - Parent extends ChildableCommand, + Parent extends ChildableCommand, > extends Command { /** Returns this command's parent. */ getParent(): Parent; diff --git a/packages/core/src/features/command/commands/abstract/ChildableCommand.ts b/packages/core/src/features/command/commands/child/ChildableCommand.ts similarity index 77% rename from packages/core/src/features/command/commands/abstract/ChildableCommand.ts rename to packages/core/src/features/command/commands/child/ChildableCommand.ts index 5fe37e24..96a86c77 100644 --- a/packages/core/src/features/command/commands/abstract/ChildableCommand.ts +++ b/packages/core/src/features/command/commands/child/ChildableCommand.ts @@ -22,30 +22,20 @@ * SOFTWARE. */ -import type { ClassImplements } from '../../../../types/ClassImplements.js'; -import type { CommandData } from '../../data/command/CommandData.js'; -import type { ChildCommand } from './ChildCommand.js'; -import type { Command } from './Command.js'; +import type { ReadonlyCollection } from '@discordjs/collection'; +import type { ClassImplements } from '../../../../types/ClassImplements'; +import type { Command } from '../Command'; +import type { ChildCommand } from './ChildCommand'; /** A command that can contain {@link ChildCommand children commands}. */ export interface ChildableCommand< - Data extends CommandData, + Data, // eslint-disable-next-line @typescript-eslint/no-explicit-any - Child extends ChildCommand, + Child extends ChildCommand, > extends Command { /** The number of children this command has. */ readonly size: number; - /** - * Adds a child to this command. - * @throws {IllegalDuplicateError} If there's already a registered child with - * this name. - * @throws {RangeError} If adding this child would make this command surpass - * its max children. See {@link getMaxChildren}. - * @throws {AssertionError} If this child's parent is not this command. - */ - addChild(child: Child): this; - /** * Adds a list of children to this command. * @throws {IllegalDuplicateError} If there's already a registered child with @@ -54,7 +44,7 @@ export interface ChildableCommand< * surpass its max children. See {@link getMaxChildren}. * @throws {AssertionError} If one of the child's parent is not this command. */ - addChildren(children: Child[]): this; + addChildren(...children: Child[]): this; /** * Removes a child by its instance. @@ -67,8 +57,7 @@ export interface ChildableCommand< /** * Removes a child by its name. * @throws {ObjectNotFoundError} If the specified child doesn't exist. - * @throws {RangeError} If removing this child would leave the command with - * no children. + * @throws {RangeError} If removing this child would leave the command with no children. */ removeChildByName(child: string): this; @@ -80,8 +69,8 @@ export interface ChildableCommand< child: SearchedChild, ): InstanceType | null; - /** Returns the children belonging to this command. */ - getChildren(): ReadonlyArray; + /** Returns the children belonging to this command, keyed by their name. */ + getChildren(): ReadonlyCollection; /** Returns the maximum amount of children that this command can hold. */ getMaxChildren(): number; diff --git a/packages/core/src/features/command/data/command/ParentCommandData.ts b/packages/core/src/features/command/commands/executable/AnyExecutableCommand.ts similarity index 78% rename from packages/core/src/features/command/data/command/ParentCommandData.ts rename to packages/core/src/features/command/commands/executable/AnyExecutableCommand.ts index 82ee5fbe..2c4ff95d 100644 --- a/packages/core/src/features/command/data/command/ParentCommandData.ts +++ b/packages/core/src/features/command/commands/executable/AnyExecutableCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,9 +22,10 @@ * SOFTWARE. */ -import type { TopLevelCommandData } from './TopLevelCommandData.js'; +import type { ApplicationCommandInteraction } from '../../interaction/ApplicationCommandInteraction'; +import type { ExecutableCommand } from './ExecutableCommand'; -/** The command data for a {@link ParentCommand}. */ -export interface ParentCommandData extends TopLevelCommandData { - /** Has no extra data for now. */ -} +export type AnyExecutableCommand = ExecutableCommand< + unknown, + ApplicationCommandInteraction +>; diff --git a/packages/core/src/features/command/data/option/CommandOptionData.ts b/packages/core/src/features/command/commands/executable/ChatExecutableCommand.ts similarity index 63% rename from packages/core/src/features/command/data/option/CommandOptionData.ts rename to packages/core/src/features/command/commands/executable/ChatExecutableCommand.ts index 693e79fa..15b2d374 100644 --- a/packages/core/src/features/command/data/option/CommandOptionData.ts +++ b/packages/core/src/features/command/commands/executable/ChatExecutableCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,13 +23,22 @@ */ import type { - ApplicationCommandOptionData, - ApplicationCommandSubCommandData, - ApplicationCommandSubGroupData, + AutocompleteInteraction, + Awaitable, + ChatInputCommandInteraction, } from 'discord.js'; +import type { CommandExecutionMeta } from '../../execution/meta/CommandExecutionMeta'; +import type { ExecutableCommand } from './ExecutableCommand'; -/** Type of option that a command can include on {@link ExecutableCommand#getOptions}. */ -export type CommandOptionData = Exclude< - ApplicationCommandOptionData, - ApplicationCommandSubGroupData | ApplicationCommandSubCommandData ->; +/** + * A command that can be executed in chat. + * Either {@link SubCommand} or {@link StandaloneCommand}. + */ +export interface ChatExecutableCommand + extends ExecutableCommand { + /** Responds to the given AutocompleteInteraction. */ + autocomplete( + interaction: AutocompleteInteraction, + metadata: CommandExecutionMeta, + ): Awaitable; +} diff --git a/packages/core/src/features/command/visitor/CommandVisitor.ts b/packages/core/src/features/command/commands/executable/ExecutableCommand.ts similarity index 50% rename from packages/core/src/features/command/visitor/CommandVisitor.ts rename to packages/core/src/features/command/commands/executable/ExecutableCommand.ts index 60f5abac..561b3814 100644 --- a/packages/core/src/features/command/visitor/CommandVisitor.ts +++ b/packages/core/src/features/command/commands/executable/ExecutableCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,25 +22,30 @@ * SOFTWARE. */ -import type { ParentCommand } from '../commands/ParentCommand.js'; -import type { StandaloneCommand } from '../commands/StandaloneCommand.js'; -import type { SubCommand } from '../commands/SubCommand.js'; -import type { SubCommandGroup } from '../commands/SubCommandGroup.js'; +import type { Awaitable } from 'discord.js'; -/** An object for visiting a command tree. */ -export interface CommandVisitor { - /** Starts visiting the command tree. */ - visit(): Returns; +import type { Filterable } from '../../../../filter/Filterable'; +import type { CommandExecutionMeta } from '../../execution/meta/CommandExecutionMeta'; +import type { CommandFilter } from '../../filter/CommandFilter'; +import type { ApplicationCommandInteraction } from '../../interaction/ApplicationCommandInteraction'; +import type { ComponentCommandInteraction } from '../../interaction/ComponentCommandInteraction'; +import type { Command } from '../Command'; - /** Visits a {@link StandaloneCommand}. */ - visitStandaloneCommand(command: StandaloneCommand): void; +/** A command that can be executed by an interaction. */ +export interface ExecutableCommand< + Data, + Interaction extends ApplicationCommandInteraction, +> extends Command, + Filterable { + /** Executes this command from an interaction. */ + execute( + interaction: Interaction, + metadata: CommandExecutionMeta, + ): Awaitable; - /** Visits a {@link ParentCommand}. */ - visitParentCommand(command: ParentCommand): void; - - /** Visits a {@link SubCommand}. */ - visitSubCommand(subcommand: SubCommand): void; - - /** Visits a {@link SubCommandGroup}. */ - visitSubCommandGroup(group: SubCommandGroup): void; + /** Handle a ComponentCommandInteraction whose customId refers to this command. */ + handleInteraction( + interaction: ComponentCommandInteraction, + metadata: CommandExecutionMeta, + ): Awaitable; } diff --git a/packages/core/src/features/command/commands/implements/ImplementsStandaloneCommand.ts b/packages/core/src/features/command/commands/implements/ImplementsStandaloneCommand.ts index 47899772..03867f81 100644 --- a/packages/core/src/features/command/commands/implements/ImplementsStandaloneCommand.ts +++ b/packages/core/src/features/command/commands/implements/ImplementsStandaloneCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ */ import type { ClassImplements } from '../../../../types/ClassImplements.js'; -import type { StandaloneCommand } from '../StandaloneCommand.js'; +import type { StandaloneCommand } from '../StandaloneCommand'; /** Type of class that implements the {@link StandaloneCommand} interface. */ export type ImplementsStandaloneCommand = ClassImplements; diff --git a/packages/core/src/features/command/customId/CommandCustomIdBuilder.ts b/packages/core/src/features/command/customId/CommandCustomIdBuilder.ts deleted file mode 100644 index 22d3cfc4..00000000 --- a/packages/core/src/features/command/customId/CommandCustomIdBuilder.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Amgelo563 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import type { MetadatableCustomIdBuilderOptions } from '../../../customId/MetadatableCustomIdBuilder'; -import { MetadatableCustomIdBuilder } from '../../../customId/MetadatableCustomIdBuilder'; -import type { CommandReferenceData } from '../resolver/CommandReferenceData'; -import { ResolvedCommandType } from '../resolver/CommandReferenceData'; - -export interface CommandCustomIdBuilderOptions - extends MetadatableCustomIdBuilderOptions { - data: CommandReferenceData; -} - -export class CommandCustomIdBuilder extends MetadatableCustomIdBuilder { - public static readonly DataIndex = 0; - - protected readonly data: CommandReferenceData; - - constructor(options: CommandCustomIdBuilderOptions) { - super(options); - - this.data = options.data; - this.setMetaAt( - CommandCustomIdBuilder.DataIndex, - JSON.stringify(options.data), - ); - } - - public static fromCommandCustomId( - string: string, - separator: string, - dataSeparator: string, - ): CommandCustomIdBuilder | null { - const builder = MetadatableCustomIdBuilder.fromMetadatableString( - string, - separator, - dataSeparator, - ); - - if (!builder) return null; - - const dataString = builder.getMetaAt(CommandCustomIdBuilder.DataIndex); - if (!dataString) return null; - - let raw: object; - try { - raw = JSON.parse(dataString); - } catch (error) { - return null; - } - - const data = CommandCustomIdBuilder.parseCommandReferenceData(raw); - if (!data) return null; - - return new CommandCustomIdBuilder({ - data, - separator, - dataSeparator, - namespace: builder.getNamespace(), - objectId: builder.getObjectId(), - }); - } - - protected static parseCommandReferenceData( - raw: any, - ): CommandReferenceData | null { - if (raw && typeof raw === 'object' && 'type' in raw && 'root' in raw) { - switch (raw.type) { - case ResolvedCommandType.StandaloneCommand: - if ('commandType' in raw && typeof raw.commandType === 'number') { - return raw; - } - break; - case ResolvedCommandType.SubCommand: - if ('subCommand' in raw && typeof raw.subCommand === 'string') { - return raw; - } - break; - case ResolvedCommandType.SubCommandOnGroup: - if ( - 'subCommand' in raw - && typeof raw.subCommand === 'string' - && 'group' in raw - && typeof raw.group === 'string' - ) { - return raw; - } - break; - } - } - - return null; - } - - public getReferenceData(): CommandReferenceData { - return this.data; - } -} diff --git a/packages/core/src/features/command/customId/CommandCustomIdCodec.ts b/packages/core/src/features/command/customId/CommandCustomIdCodec.ts index 4e5125c6..68ec2df1 100644 --- a/packages/core/src/features/command/customId/CommandCustomIdCodec.ts +++ b/packages/core/src/features/command/customId/CommandCustomIdCodec.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,13 +25,11 @@ import type { CustomIdBuilder } from '../../../customId/CustomIdBuilder'; import type { CustomIdCodec } from '../../../customId/CustomIdCodec.js'; import type { StringIterator } from '../../../string/StringIterator'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; -import type { CommandReferenceData } from '../resolver/CommandReferenceData'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; /** An object responsible for creating and manipulating customIds that refer to command names. */ export interface CommandCustomIdCodec - extends CustomIdCodec> { + extends CustomIdCodec { /** * Creates a {@link CustomIdBuilder} that can be used to start a * customId that refers to the passed command. @@ -40,11 +38,10 @@ export interface CommandCustomIdCodec * // userinfo command, with 'Amgelo#1106' as an extra data. * * const commandManager = myBot.commands; - * - * const commandRouter = commandManager.getRouter(); + * const repository = commandManager.getRepository(); * * const userInfoCommand = - * commandRouter.locateCommandByTree(UserInfoCommandClass); + * repository.locateCommandByTree(UserInfoCommandClass); * if (!userInfoCommand) return; * * const customIdCodec = commandManager.getCustomIdCodec(); @@ -55,9 +52,7 @@ export interface CommandCustomIdCodec * // Use this for your component's customId. * const customId: string = builder.build(); */ - createCustomIdBuilder( - command: ExecutableCommand, - ): CustomIdBuilder; + createCustomIdBuilder(command: AnyExecutableCommand): CustomIdBuilder; /** * Creates a {@link StringIterator} from a command customId, leaving only the @@ -76,6 +71,6 @@ export interface CommandCustomIdCodec */ createIteratorFromCustomId(commandCustomId: string): StringIterator | null; - /** Extracts the reference data from the passed command customId. */ - deserializeToData(customId: string): CommandReferenceData | null; + /** Returns the separator used to serialize names in command customIds. */ + getNamesSeparator(): string; } diff --git a/packages/core/src/features/command/data/command/SubCommandGroupData.ts b/packages/core/src/features/command/customId/SerializedCommandData.ts similarity index 84% rename from packages/core/src/features/command/data/command/SubCommandGroupData.ts rename to packages/core/src/features/command/customId/SerializedCommandData.ts index e8cc1770..526a29cc 100644 --- a/packages/core/src/features/command/data/command/SubCommandGroupData.ts +++ b/packages/core/src/features/command/customId/SerializedCommandData.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,10 @@ * SOFTWARE. */ -import type { CommandData } from './CommandData.js'; +import type { ApplicationCommandType } from 'discord.js'; -/** The command data for a {@link SubCommandGroup}. */ -export interface SubCommandGroupData extends CommandData {} +export interface SerializedCommandData { + id: string; + type: ApplicationCommandType; + childId: string | null; +} diff --git a/packages/core/src/features/command/data/command/CommandData.ts b/packages/core/src/features/command/data/command/CommandData.ts deleted file mode 100644 index a9235145..00000000 --- a/packages/core/src/features/command/data/command/CommandData.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Amgelo563 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import type { LocalizationMap } from 'discord.js'; - -/** The base command data that all command objects share, used to register them at Discord. */ -export interface CommandData { - /** The command name, must be in lowercase. */ - name: string; - /** The localizations for the command name. */ - nameLocalizations?: LocalizationMap; - /** The command description. */ - description: string; - /** The localizations for the command description. */ - descriptionLocalizations?: LocalizationMap; -} diff --git a/packages/core/src/features/command/deploy/CommandDeployer.ts b/packages/core/src/features/command/deploy/CommandDeployer.ts new file mode 100644 index 00000000..8cb9e677 --- /dev/null +++ b/packages/core/src/features/command/deploy/CommandDeployer.ts @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2024 Amgelo563 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { ReadonlyCollection } from '@discordjs/collection'; +import type { ApplicationCommand, Awaitable } from 'discord.js'; + +import type { TopLevelCommand } from '../commands/TopLevelCommand'; + +export interface CommandDeployer extends IterableIterator { + /** + * Notifies the deployer about the Client start, and deploys the commands. + * + * @throws {IllegalStateError} If the deployer has already started. + * @throws {Error} If there was an error while deploying the commands. + */ + start(): Awaitable; + + /** + * Registers commands on Discord if the deployer has started, otherwise + * saves them for later. + * + * @throws {AssertionError} If a command is already registered. + * @throws {Error} If there was an error while building or deploying the commands. + */ + addCommands(...commands: TopLevelCommand[]): Awaitable; + + /** + * Unregisters commands on Discord if the deployer has started, otherwise + * removes them from the pending add list. + * + * @throws {ObjectNotFoundError} If that command is not deployed or in the pending add list. + * @throws {Error} If there was an error while undeploying the commands. + */ + removeCommands(...commands: TopLevelCommand[]): Awaitable; + + /** + * Edits commands on Discord. Requires the deployer to have started. + * + * @throws {IllegalStateError} If the deployer has not started. + * @throws {ObjectNotFoundError} If that command is not deployed. + * @throws {Error} If there was an error while editing the commands. + */ + editCommands(...commands: TopLevelCommand[]): Awaitable; + + /** + * Returns the Discord ApplicationCommands that have been deployed, + * keyed by the command ID. + * + * Use them only to extract data. Prefer this object's (or the CommandManager's) + * methods to add/remove/edit commands. + */ + getMappings(): ReadonlyCollection; + + /** Returns an iterator for the deployed ApplicationCommands. */ + values(): IterableIterator; + + /** Returns an iterator for the deployed commands' IDs. */ + keys(): IterableIterator; + + /** Returns an iterator for the deployed commands. */ + entries(): IterableIterator<[string, ApplicationCommand]>; +} diff --git a/packages/core/src/features/command/data/command/SubCommandData.ts b/packages/core/src/features/command/deploy/ReadonlyCommandDeployer.ts similarity index 83% rename from packages/core/src/features/command/data/command/SubCommandData.ts rename to packages/core/src/features/command/deploy/ReadonlyCommandDeployer.ts index 5b8ed7d8..46d1c794 100644 --- a/packages/core/src/features/command/data/command/SubCommandData.ts +++ b/packages/core/src/features/command/deploy/ReadonlyCommandDeployer.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,9 @@ * SOFTWARE. */ -import type { CommandData } from './CommandData.js'; +import type { CommandDeployer } from './CommandDeployer'; -/** The command data for a {@link SubCommand}. */ -export interface SubCommandData extends Omit {} +export type ReadonlyCommandDeployer = Omit< + CommandDeployer, + 'addCommands' | 'removeCommands' | 'start' | 'editCommands' +>; diff --git a/packages/core/src/features/command/error/CommandErrorHandler.ts b/packages/core/src/features/command/error/CommandErrorHandler.ts index aba0833f..303749b9 100644 --- a/packages/core/src/features/command/error/CommandErrorHandler.ts +++ b/packages/core/src/features/command/error/CommandErrorHandler.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,9 +23,8 @@ */ import type { ErrorHandler } from '../../../error/handler/ErrorHandler.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; import type { CommandExecutionArgs } from '../execution/args/CommandExecutionArgs.js'; export interface CommandErrorHandler - extends ErrorHandler, CommandExecutionArgs> {} + extends ErrorHandler {} diff --git a/packages/core/src/features/command/errors/CommandAutocompleteError.ts b/packages/core/src/features/command/errors/CommandAutocompleteError.ts index 72d77ed5..107ae971 100644 --- a/packages/core/src/features/command/errors/CommandAutocompleteError.ts +++ b/packages/core/src/features/command/errors/CommandAutocompleteError.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,8 +24,7 @@ import type { AutocompleteInteraction } from 'discord.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; import type { CommandExecutionMeta } from '../execution/meta/CommandExecutionMeta.js'; import { CommandError } from './CommandError.js'; @@ -35,7 +34,7 @@ export class CommandAutocompleteError extends CommandError { constructor( error: Error, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: AutocompleteInteraction, meta: CommandExecutionMeta, message?: string, diff --git a/packages/core/src/features/command/errors/CommandAutocompleteRespondError.ts b/packages/core/src/features/command/errors/CommandAutocompleteRespondError.ts index 5c242d05..3e31483d 100644 --- a/packages/core/src/features/command/errors/CommandAutocompleteRespondError.ts +++ b/packages/core/src/features/command/errors/CommandAutocompleteRespondError.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,16 +23,15 @@ */ import type { AutocompleteInteraction } from 'discord.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; +import type { CommandExecutionMeta } from '../execution/meta/CommandExecutionMeta.js'; import { CommandAutocompleteError } from './CommandAutocompleteError.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; -import type { CommandExecutionMeta } from '../execution/meta/CommandExecutionMeta.js'; export class CommandAutocompleteRespondError extends CommandAutocompleteError { constructor( error: Error, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: AutocompleteInteraction, meta: CommandExecutionMeta, message?: string, diff --git a/packages/core/src/features/command/errors/CommandError.ts b/packages/core/src/features/command/errors/CommandError.ts index 1f15e15f..0c371460 100644 --- a/packages/core/src/features/command/errors/CommandError.ts +++ b/packages/core/src/features/command/errors/CommandError.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,20 +23,19 @@ */ import { FeatureError } from '../../../errors/FeatureError.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; import type { CommandExecutionMeta } from '../execution/meta/CommandExecutionMeta.js'; import type { CommandResolvableInteraction } from '../interaction/CommandResolvableInteraction.js'; /** An Error that wraps errors that occur during the execution of an {@link ExecutableCommand} object. */ -export class CommandError extends FeatureError> { +export class CommandError extends FeatureError { protected readonly interaction: CommandResolvableInteraction; protected readonly meta: CommandExecutionMeta; constructor( error: Error, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandResolvableInteraction, meta: CommandExecutionMeta, message?: string, diff --git a/packages/core/src/features/command/events/CommandEvent.ts b/packages/core/src/features/command/events/CommandEvent.ts index 0fe0ee05..fd192871 100644 --- a/packages/core/src/features/command/events/CommandEvent.ts +++ b/packages/core/src/features/command/events/CommandEvent.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,11 +23,10 @@ */ import type { AutocompleteInteraction } from 'discord.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; +import type { TopLevelCommand } from '../commands/TopLevelCommand.js'; import type { CommandExecutionMeta } from '../execution/meta/CommandExecutionMeta.js'; import type { CommandExecutableInteraction } from '../interaction/CommandExecutableInteraction.js'; -import type { TopLevelCommand } from '../commands/TopLevelCommand.js'; /** Enum of possible command events. */ export const CommandEventEnum = { @@ -46,12 +45,12 @@ export interface CommandEventArgs { commandAdd: [command: TopLevelCommand]; commandRemove: [command: TopLevelCommand]; commandRun: [ - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, meta: CommandExecutionMeta, ]; commandAutocomplete: [ - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: AutocompleteInteraction, meta: CommandExecutionMeta, ]; diff --git a/packages/core/src/features/command/execution/executor/CommandExecutor.ts b/packages/core/src/features/command/execution/executor/CommandExecutor.ts index 306fdb81..b8eaabc7 100644 --- a/packages/core/src/features/command/execution/executor/CommandExecutor.ts +++ b/packages/core/src/features/command/execution/executor/CommandExecutor.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,9 +32,9 @@ import type { import type { ErrorHandlerContainer } from '../../../../error/handler/ErrorHandlerContainer.js'; import type { MiddlewareListContainer } from '../../../../middleware/list/MiddlewareListContainer.js'; -import type { ExecutableCommand } from '../../commands/abstract/ExecutableCommand.js'; -import type { StandaloneCommand } from '../../commands/StandaloneCommand.js'; -import type { CommandData } from '../../data/command/CommandData.js'; +import type { ContextMenuCommand } from '../../commands/ContextMenuCommand'; +import type { AnyExecutableCommand } from '../../commands/executable/AnyExecutableCommand'; +import type { ChatExecutableCommand } from '../../commands/executable/ChatExecutableCommand'; import type { CommandErrorHandler } from '../../error/CommandErrorHandler.js'; import type { CommandExecutableInteraction } from '../../interaction/CommandExecutableInteraction.js'; import type { ComponentCommandInteraction } from '../../interaction/ComponentCommandInteraction.js'; @@ -46,42 +46,42 @@ export interface CommandExecutor extends ErrorHandlerContainer, MiddlewareListContainer { execute( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, metadata: CommandExecutionMeta, ): Awaitable; /** Executes a {@link ChatInputCommandInteraction} on a {@link ExecutableCommand}. */ executeChatInput( - command: ExecutableCommand, + command: ChatExecutableCommand, interaction: ChatInputCommandInteraction, metadata: CommandExecutionMeta, ): Awaitable; /** Executes a {@link ComponentCommandInteraction} on a {@link ExecutableCommand}. */ executeComponent( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: ComponentCommandInteraction, metadata: CommandExecutionMeta, ): Awaitable; /** Autocompletes (responds) a {@link AutocompleteInteraction} with the provided options from {@link ExecutableCommand#autocomplete}. */ autocomplete( - command: ExecutableCommand, + command: ChatExecutableCommand, interaction: AutocompleteInteraction, metadata: CommandExecutionMeta, ): Awaitable; /** Executes a {@link UserContextMenuCommandInteraction} on a {@link StandaloneCommand}. */ executeUser( - command: StandaloneCommand, + command: ContextMenuCommand, interaction: UserContextMenuCommandInteraction, metadata: CommandExecutionMeta, ): Awaitable; /** Executes a {@link MessageContextMenuCommandInteraction} on a {@link StandaloneCommand}. */ executeMessage( - command: StandaloneCommand, + command: ContextMenuCommand, interaction: MessageContextMenuCommandInteraction, metadata: CommandExecutionMeta, ): Awaitable; diff --git a/packages/core/src/features/command/execution/meta/CommandExecutionMeta.ts b/packages/core/src/features/command/execution/meta/CommandExecutionMeta.ts index d8656c7c..847b2a2e 100644 --- a/packages/core/src/features/command/execution/meta/CommandExecutionMeta.ts +++ b/packages/core/src/features/command/execution/meta/CommandExecutionMeta.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,8 +28,7 @@ import type { NyxBot } from '../../../../bot/NyxBot.js'; import type { Identifiable } from '../../../../identity/Identifiable.js'; import type { Identifier } from '../../../../identity/Identifier.js'; import type { MetaCollection } from '../../../../meta/MetaCollection.js'; -import type { ExecutableCommand } from '../../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../../commands/executable/AnyExecutableCommand'; import type { CommandExecutableInteraction } from '../../interaction/CommandExecutableInteraction.js'; /** An object that saves metadata about a command execution. */ @@ -51,11 +50,11 @@ export class CommandExecutionMeta /** Creates an CommandExecutionMeta using the given arguments, and an autogenerated ID. */ public static fromCommandCall( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, bot: NyxBot, ): CommandExecutionMeta { - const name = command.getReadableName(); + const name = command.constructor.name; const id = Symbol( `Command '${name}' | Interaction '${interaction.id}' | @${Date.now()}`, ); @@ -64,11 +63,11 @@ export class CommandExecutionMeta /** Creates an CommandExecutionMeta using the given arguments, and an autogenerated ID. */ public static fromCommandAutocomplete( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: AutocompleteInteraction, bot: NyxBot, ): CommandExecutionMeta { - const name = command.getReadableName(); + const name = command.constructor.name; const interactionId = interaction.id; const option = interaction.options.getFocused(); diff --git a/packages/core/src/features/command/filter/CommandFilter.ts b/packages/core/src/features/command/filter/CommandFilter.ts index f600fe5c..b199f0c8 100644 --- a/packages/core/src/features/command/filter/CommandFilter.ts +++ b/packages/core/src/features/command/filter/CommandFilter.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,10 +23,9 @@ */ import type { Filter } from '../../../filter/Filter.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; import type { CommandExecutionArgs } from '../execution/args/CommandExecutionArgs.js'; /** {@link Filter} that can filter an {@link ExecutableCommand}'s execution. */ -export interface CommandFilter - extends Filter, CommandExecutionArgs> {} +export interface CommandFilter + extends Filter {} diff --git a/packages/core/src/features/command/middleware/CommandMiddleware.ts b/packages/core/src/features/command/middleware/CommandMiddleware.ts index f515c6ef..919bf4bb 100644 --- a/packages/core/src/features/command/middleware/CommandMiddleware.ts +++ b/packages/core/src/features/command/middleware/CommandMiddleware.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,10 +23,9 @@ */ import type { Middleware } from '../../../middleware/Middleware.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; import type { CommandExecutionArgs } from '../execution/args/CommandExecutionArgs.js'; /** {@link Middleware} that can handle {@link ExecutableCommand} executions. */ export interface CommandMiddleware - extends Middleware, CommandExecutionArgs> {} + extends Middleware {} diff --git a/packages/core/src/features/command/middleware/errors/CommandMiddlewareError.ts b/packages/core/src/features/command/middleware/errors/CommandMiddlewareError.ts index aaf4278d..e3d3c053 100644 --- a/packages/core/src/features/command/middleware/errors/CommandMiddlewareError.ts +++ b/packages/core/src/features/command/middleware/errors/CommandMiddlewareError.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,7 @@ * SOFTWARE. */ -import type { ExecutableCommand } from '../../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../../commands/executable/AnyExecutableCommand'; import { CommandError } from '../../errors/CommandError.js'; import type { CommandExecutionMeta } from '../../execution/meta/CommandExecutionMeta.js'; import type { CommandResolvableInteraction } from '../../interaction/CommandResolvableInteraction.js'; @@ -35,7 +34,7 @@ export class CommandMiddlewareError extends CommandError { constructor( error: Error, middleware: CommandMiddleware, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandResolvableInteraction, meta: CommandExecutionMeta, ) { diff --git a/packages/core/src/features/command/middleware/errors/UncaughtCommandMiddlewareError.ts b/packages/core/src/features/command/middleware/errors/UncaughtCommandMiddlewareError.ts index 511efa1e..ba0f3732 100644 --- a/packages/core/src/features/command/middleware/errors/UncaughtCommandMiddlewareError.ts +++ b/packages/core/src/features/command/middleware/errors/UncaughtCommandMiddlewareError.ts @@ -23,8 +23,7 @@ */ import type { MiddlewareList } from '../../../../middleware/list/MiddlewareList'; -import type { ExecutableCommand } from '../../commands/abstract/ExecutableCommand.js'; -import type { CommandData } from '../../data/command/CommandData.js'; +import type { AnyExecutableCommand } from '../../commands/executable/AnyExecutableCommand'; import { CommandError } from '../../errors/CommandError.js'; import type { CommandExecutionMeta } from '../../execution/meta/CommandExecutionMeta.js'; import type { CommandExecutableInteraction } from '../../interaction/CommandExecutableInteraction.js'; @@ -36,7 +35,7 @@ export class UncaughtCommandMiddlewareError extends CommandError { constructor( error: Error, middlewareList: MiddlewareList, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, meta: CommandExecutionMeta, ) { diff --git a/packages/core/src/features/command/repository/CommandRepository.ts b/packages/core/src/features/command/repository/CommandRepository.ts index 6a5de8d5..87df47d8 100644 --- a/packages/core/src/features/command/repository/CommandRepository.ts +++ b/packages/core/src/features/command/repository/CommandRepository.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,77 +23,85 @@ */ import type { ReadonlyCollection } from '@discordjs/collection'; -import type { ApplicationCommand, Awaitable } from 'discord.js'; -import type { ArrayMinLength } from '../../../types/ArrayMinLength'; - -import type { BotLifecycleObserver } from '../../../types/BotLifecycleObserver'; import type { ClassImplements } from '../../../types/ClassImplements.js'; -import type { Command } from '../commands/abstract/Command.js'; -import type { ExecutableCommand } from '../commands/abstract/ExecutableCommand.js'; +import type { Command } from '../commands/Command'; import type { ImplementsParentCommand } from '../commands/implements/ImplementsParentCommand.js'; import type { ImplementsStandaloneCommand } from '../commands/implements/ImplementsStandaloneCommand.js'; import type { ImplementsSubCommand } from '../commands/implements/ImplementsSubCommand.js'; import type { ImplementsSubCommandGroup } from '../commands/implements/ImplementsSubCommandGroup.js'; -import type { ParentCommand } from '../commands/ParentCommand'; +import type { SubCommand } from '../commands/SubCommand'; +import type { SubCommandGroup } from '../commands/SubCommandGroup'; import type { TopLevelCommand } from '../commands/TopLevelCommand.js'; -import type { CommandData } from '../data/command/CommandData.js'; -import type { CommandReferenceData } from '../resolver/CommandReferenceData.js'; -/** An object responsible for storing commands and their correspondent Discord application mappings. */ +/** An object responsible for storing commands. */ export interface CommandRepository - extends BotLifecycleObserver, - IterableIterator<[string, TopLevelCommand]> { + extends IterableIterator<[string, TopLevelCommand]> { /** Returns the number of stored commands. */ readonly size: number; /** - * Adds a {@link TopLevelCommand command} and registers it on Discord, if the - * bot has started. + * Adds a {@link TopLevelCommand command}. * * @throws {IllegalDuplicateError} If a command with that same data exists. */ - addCommand(command: TopLevelCommand): Awaitable; + addCommand(command: TopLevelCommand): this; /** - * Removes a {@link TopLevelCommand command} and unregisters it from Discord, - * if the bot has started. + * Removes a {@link TopLevelCommand command}. * * @throws {ObjectNotFoundError} If that command is not registered. */ - removeCommand(command: TopLevelCommand): Awaitable; - - /** Updates a parent command on Discord, after its children have changed. */ - updateParentCommand(command: ParentCommand): Awaitable; + removeCommand(command: TopLevelCommand): this; /** Returns whether the passed ID belongs to a registered command. */ isCommandId(id: string): boolean; /** Returns whether the passed command is registered. */ - isCommandInstance(instance: Command): boolean; + isCommandInstance(instance: TopLevelCommand): boolean; - /** Returns the command associated with the passed ID. */ - getCommandById(id: string): Command | null; + /** Returns the command associated with the passed name. */ + getCommandByName(id: string): TopLevelCommand | null; - /** Locates an executable command by its reference data. */ - locateByData( - data: CommandReferenceData, - ): ExecutableCommand | null; + /** + * Locates a {@link TopLevelCommand} by its name tree. + * Equal to {@link CommandRepository#getCommandByName}. + */ + locateByNameTree(name: string): TopLevelCommand | null; + + /** Locates a {@link SubCommand} or {@link SubCommandGroup} by its name tree. */ + locateByNameTree( + parent: string, + child: string, + ): SubCommand | SubCommandGroup | null; + + /** Locates a {@link SubCommand} by its name tree. */ + locateByNameTree( + parent: string, + group: string, + subCommand: string, + ): SubCommand | null; + + /** Locates a {@link Command} by its name tree. */ + locateByNameTree( + parent: string, + firstChild?: string, + secondChild?: string, + ): TopLevelCommand | SubCommand | SubCommandGroup | null; /** - * Locates a {@link SubCommandGroup} by its declaration tree, passing its + * Locates a {@link SubCommandGroup} by its class tree, passing its * parent and its class and returning the stored instance. * @example * // Suppose the structure: * // - SomeParentCommand * // -- SomeSubCommandGroup * - * const locate = this.bot.commands.getRepository().locateByTree; + * const repo = this.bot.commands.getRepository(); * - * locate(SomeSubCommandGroup); // null - * locate(SomeParentCommand, SomeSubCommandGroup); // Stored instance of - * SomeSubCommandGroup + * repo.locateByClassTree(SomeSubCommandGroup); // null + * repo.locateByClassTree(SomeParentCommand, SomeSubCommandGroup); // Stored instance of SomeSubCommandGroup */ - locateByTree( + locateByClassTree( ParentCommandClass: ImplementsParentCommand, SubCommandGroupClass: T, ): InstanceType | null; @@ -107,14 +115,13 @@ export interface CommandRepository * // -- SomeSubCommandGroup * // --- ChildSubCommand * - * const locate = this.bot.commands.getRepository().locateByTtre; + * const repo = this.bot.commands.getRepository(); * - * locate(ChildSubCommand); // null - * locate(SomeParentCommand, ChildSubCommand); // null - * locate(SomeParentCommand, SomeSubCommandGroup, ChildSubCommand); // Stored - * instance of ChildSubCommand + * repo.locateByClassTree(ChildSubCommand); // null + * repo.locateByClassTree(SomeParentCommand, ChildSubCommand); // null + * repo.locateByClassTree(SomeParentCommand, SomeSubCommandGroup, ChildSubCommand); // Stored instance of ChildSubCommand */ - locateByTree( + locateByClassTree( ParentCommandClass: ImplementsParentCommand, SubCommandGroupClass: ImplementsSubCommandGroup, SubCommandClass: T, @@ -128,13 +135,12 @@ export interface CommandRepository * // - SomeParentCommand * // -- SomeSubCommand * - * const locate = this.bot.commands.getRepository().locateByTtre; + * const repo = this.bot.commands.getRepository(); * - * locate(SomeSubCommand); // null - * locate(SomeParentCommand, SomeSubCommand); // Stored instance of - * SomeSubCommand + * repo.locateByClassTree(SomeSubCommand); // null + * repo.locateByClassTree(SomeParentCommand, SomeSubCommand); // Stored instance of SomeSubCommand */ - locateByTree( + locateByClassTree( ParentCommandClass: ImplementsParentCommand, SubCommandClass: T, ): InstanceType | null; @@ -146,11 +152,11 @@ export interface CommandRepository * // Suppose the structure: * // - SomeParentCommand * - * const locate = this.bot.commands.getRepository().locateByTree; + * const repo = this.bot.commands.getRepository(); * - * locate(SomeParentCommand); // Stored instance of SomeParentCommand + * repo.locateByClassTree(SomeParentCommand); // Stored instance of SomeParentCommand */ - locateByTree( + locateByClassTree( ParentCommandClass: T, ): InstanceType | null; @@ -161,11 +167,11 @@ export interface CommandRepository * // Suppose the structure: * // - SomeStandaloneCommand * - * const locate = this.bot.commands.getRepository().locateByTree; + * const repo = this.bot.commands.getRepository(); * - * locate(SomeStandaloneCommand); // Stored instance of SomeStandaloneCommand + * repo.locateByClassTree(SomeStandaloneCommand); // Stored instance of SomeStandaloneCommand */ - locateByTree( + locateByClassTree( StandaloneCommandClass: T, ): InstanceType | null; @@ -180,14 +186,14 @@ export interface CommandRepository * // -- SomeSubCommandGroup * // --- ChildSubCommand * - * const locate = this.bot.commands.getRepository().locateByTree; + * const repo = this.bot.commands.getRepository(); * - * locate(SomeStandaloneCommand); // Stored instance of SomeStandaloneCommand - * locate(SomeParentCommand); // Stored instance of SomeParentCommand - * locate(SomeParentCommand, SomeSubCommandGroup); // Stored instance of - * SomeSubCommandGroup locate(SomeParentCommand, ChildSubCommand); // null + * repo.locateByClassTree(SomeStandaloneCommand); // Stored instance of SomeStandaloneCommand + * repo.locateByClassTree(SomeParentCommand); // Stored instance of SomeParentCommand + * repo.locateByClassTree(SomeParentCommand, SomeSubCommandGroup); // Stored instance of SomeSubCommandGroup + * repo.locateByClassTree(SomeParentCommand, ChildSubCommand); // null */ - locateByTree>>( + locateByClassTree>>( TopLevelCommandClass: ClassImplements, FirstChildClass?: ImplementsSubCommandGroup | ImplementsSubCommand, SecondChildClass?: ImplementsSubCommand, @@ -196,12 +202,6 @@ export interface CommandRepository /** Returns the currently stored top level commands, keyed by their ID. */ getCommands(): ReadonlyCollection; - /** Returns the Discord Application mappings of the currently stored commands. */ - getMappings(): ReadonlyCollection< - string, - ArrayMinLength - >; - values(): IterableIterator; keys(): IterableIterator; diff --git a/packages/core/src/features/command/resolver/CommandResolver.ts b/packages/core/src/features/command/resolve/CommandResolver.ts similarity index 74% rename from packages/core/src/features/command/resolver/CommandResolver.ts rename to packages/core/src/features/command/resolve/CommandResolver.ts index 2fdeef38..bba2f5ed 100644 --- a/packages/core/src/features/command/resolver/CommandResolver.ts +++ b/packages/core/src/features/command/resolve/CommandResolver.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,19 +22,23 @@ * SOFTWARE. */ -import type { ApplicationCommandInteraction } from '../interaction/ApplicationCommandInteraction.js'; -import type { CommandReferenceData } from './CommandReferenceData.js'; import type { AutocompleteInteraction } from 'discord.js'; +import type { AnyExecutableCommand } from '../commands/executable/AnyExecutableCommand'; +import type { ApplicationCommandInteraction } from '../interaction/ApplicationCommandInteraction.js'; +import type { CommandRepository } from '../repository/CommandRepository'; + /** An object responsible for locating command objects given a command compatible interaction. */ export interface CommandResolver { - /** Resolves a {@link CommandReferenceData} that the given application interaction refers to. */ + /** Resolves an {@link ExecutableCommand} that the given application interaction refers to. */ resolveFromCommandInteraction( interaction: ApplicationCommandInteraction, - ): CommandReferenceData | null; + repository: CommandRepository, + ): AnyExecutableCommand | null; - /** Resolves a {@link CommandReferenceData} that the given autocomplete interaction refers to. */ + /** Resolves an {@link ExecutableCommand} that the given autocomplete interaction refers to. */ resolveFromAutocompleteInteraction( interaction: AutocompleteInteraction, - ): CommandReferenceData | null; + repository: CommandRepository, + ): AnyExecutableCommand | null; } diff --git a/packages/core/src/features/command/resolver/CommandReferenceData.ts b/packages/core/src/features/command/resolver/CommandReferenceData.ts deleted file mode 100644 index ff5578e1..00000000 --- a/packages/core/src/features/command/resolver/CommandReferenceData.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Amgelo563 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import type { ApplicationCommandType } from 'discord.js'; - -/** Type of resolved command on a command interaction. */ -export enum ResolvedCommandType { - StandaloneCommand = 'standaloneCommand', - SubCommand = 'subCommand', - SubCommandOnGroup = 'subCommandOnGroup', -} - -/** Base data of a resolved command on a command interaction. */ -export interface BaseCommandReferenceData { - type: Type; - root: string; -} - -/** Data of a resolved subcommand on a command interaction. */ -export interface SubCommandReferenceData - extends BaseCommandReferenceData { - subCommand: string; -} - -/** Data of a resolved subcommand on a group on a command interaction. */ -export interface SubCommandOnGroupReferenceData - extends BaseCommandReferenceData { - subCommand: string; - group: string; -} - -/** Data of a resolved standalone command on a command interaction. */ -export interface StandaloneCommandReferenceData - extends BaseCommandReferenceData { - commandType: ApplicationCommandType; -} - -/** Data of a resolved command on a command interaction. */ -export type CommandReferenceData = - | StandaloneCommandReferenceData - | SubCommandReferenceData - | SubCommandOnGroupReferenceData; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index aec1e7cc..78743775 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,28 +19,26 @@ export * from './errors/IllegalStateError'; export * from './errors/ObjectNotFoundError'; export * from './features/command/CommandManager'; export * from './features/command/application/ApplicationCommandCollection'; +export * from './features/command/commands/Command'; +export * from './features/command/commands/ContextMenuCommand'; export * from './features/command/commands/ParentCommand'; export * from './features/command/commands/StandaloneCommand'; export * from './features/command/commands/SubCommand'; export * from './features/command/commands/SubCommandGroup'; export * from './features/command/commands/TopLevelCommand'; -export * from './features/command/commands/abstract/ChildableCommand'; -export * from './features/command/commands/abstract/ChildCommand'; -export * from './features/command/commands/abstract/Command'; -export * from './features/command/commands/abstract/ExecutableCommand'; +export * from './features/command/commands/child/ChildableCommand'; +export * from './features/command/commands/child/ChildCommand'; +export * from './features/command/commands/executable/AnyExecutableCommand'; +export * from './features/command/commands/executable/ChatExecutableCommand'; +export * from './features/command/commands/executable/ExecutableCommand'; export * from './features/command/commands/implements/ImplementsParentCommand'; export * from './features/command/commands/implements/ImplementsStandaloneCommand'; export * from './features/command/commands/implements/ImplementsSubCommand'; export * from './features/command/commands/implements/ImplementsSubCommandGroup'; -export * from './features/command/customId/CommandCustomIdBuilder'; export * from './features/command/customId/CommandCustomIdCodec'; -export * from './features/command/data/command/CommandData'; -export * from './features/command/data/command/ParentCommandData'; -export * from './features/command/data/command/StandaloneCommandData'; -export * from './features/command/data/command/SubCommandData'; -export * from './features/command/data/command/SubCommandGroupData'; -export * from './features/command/data/command/TopLevelCommandData'; -export * from './features/command/data/option/CommandOptionData'; +export * from './features/command/customId/SerializedCommandData'; +export * from './features/command/deploy/CommandDeployer'; +export * from './features/command/deploy/ReadonlyCommandDeployer'; export * from './features/command/error/CommandErrorHandler'; export * from './features/command/errors/CommandAutocompleteError'; export * from './features/command/errors/CommandAutocompleteRespondError'; @@ -60,9 +58,7 @@ export * from './features/command/middleware/errors/CommandMiddlewareError'; export * from './features/command/middleware/errors/UncaughtCommandMiddlewareError'; export * from './features/command/repository/CommandRepository'; export * from './features/command/repository/ReadonlyCommandRepository'; -export * from './features/command/resolver/CommandReferenceData'; -export * from './features/command/resolver/CommandResolver'; -export * from './features/command/visitor/CommandVisitor'; +export * from './features/command/resolve/CommandResolver'; export * from './features/event/EventManager'; export * from './features/event/bus/AnyEventBus'; export * from './features/event/bus/EventBus'; diff --git a/packages/framework/src/bot/Bot.ts b/packages/framework/src/bot/Bot.ts index 2ba2d819..a1cf0f5c 100644 --- a/packages/framework/src/bot/Bot.ts +++ b/packages/framework/src/bot/Bot.ts @@ -72,7 +72,7 @@ type BotOptionsWithDefaults< ConcretePluginManager, ConcreteBotService >, - 'logger' | 'client' | 'id' | 'token' | 'refreshCommands' + 'logger' | 'client' | 'id' | 'token' >; /** The main Bot class. */ @@ -169,7 +169,6 @@ export class Bot< bot, generatedOptions.id, generatedOptions.client, - generatedOptions.refreshCommands, ); return { ...defaultOptions, ...generator(bot) } as BotOptions< @@ -188,7 +187,6 @@ export class Bot< bot: NyxBot, id: Identifier, client: Client, - refreshCommands: boolean, ) => { const eventManager = DefaultEventManager.create(bot, client); @@ -198,7 +196,6 @@ export class Bot< bot, client, eventManager.getClientBus(), - refreshCommands, ), schedules: DefaultScheduleManager.create(bot), sessions: DefaultSessionManager.create(bot), diff --git a/packages/framework/src/customId/BasicCustomIdCodec.ts b/packages/framework/src/customId/AbstractCustomIdCodec.ts similarity index 93% rename from packages/framework/src/customId/BasicCustomIdCodec.ts rename to packages/framework/src/customId/AbstractCustomIdCodec.ts index 732a10fa..4f76e3ed 100644 --- a/packages/framework/src/customId/BasicCustomIdCodec.ts +++ b/packages/framework/src/customId/AbstractCustomIdCodec.ts @@ -22,18 +22,14 @@ * SOFTWARE. */ -import type { - CustomIdBuilder, - CustomIdCodec, - Identifiable, -} from '@nyx-discord/core'; +import type { CustomIdBuilder, CustomIdCodec } from '@nyx-discord/core'; import { AssertionError, MetadatableCustomIdBuilder, StringIterator, } from '@nyx-discord/core'; -export class BasicCustomIdCodec> +export abstract class AbstractCustomIdCodec implements CustomIdCodec { public static readonly DefaultSeparator: string = 'แ‚ž'; @@ -63,7 +59,7 @@ export class BasicCustomIdCodec> } public createCustomIdBuilder(serialized: Serialized): CustomIdBuilder { - const id = serialized.getId(); + const id = this.getIdOf(serialized); return new MetadatableCustomIdBuilder({ namespace: this.namespace, @@ -110,4 +106,6 @@ export class BasicCustomIdCodec> public getDataSeparator(): string { return this.dataSeparator; } + + protected abstract getIdOf(serialized: Serialized): string; } diff --git a/packages/core/src/features/command/data/command/StandaloneCommandData.ts b/packages/framework/src/customId/IdentifiableCustomIdCodec.ts similarity index 74% rename from packages/core/src/features/command/data/command/StandaloneCommandData.ts rename to packages/framework/src/customId/IdentifiableCustomIdCodec.ts index f8d2f572..befdd197 100644 --- a/packages/core/src/features/command/data/command/StandaloneCommandData.ts +++ b/packages/framework/src/customId/IdentifiableCustomIdCodec.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,14 @@ * SOFTWARE. */ -import type { TopLevelCommandData } from './TopLevelCommandData.js'; +import type { Identifiable } from '@nyx-discord/core'; -/** The command data for a {@link StandaloneCommand}. */ -export interface StandaloneCommandData extends TopLevelCommandData {} +import { AbstractCustomIdCodec } from './AbstractCustomIdCodec'; + +export class IdentifiableCustomIdCodec< + Serialized extends Identifiable, +> extends AbstractCustomIdCodec { + protected getIdOf(serialized: Serialized): string { + return serialized.getId(); + } +} diff --git a/packages/framework/src/features/command/DefaultCommandManager.ts b/packages/framework/src/features/command/DefaultCommandManager.ts index dc6f1e65..72b7fa75 100644 --- a/packages/framework/src/features/command/DefaultCommandManager.ts +++ b/packages/framework/src/features/command/DefaultCommandManager.ts @@ -23,8 +23,9 @@ */ import type { + AnyExecutableCommand, CommandCustomIdCodec, - CommandData, + CommandDeployer, CommandEventArgs, CommandExecutableInteraction, CommandExecutor, @@ -34,9 +35,8 @@ import type { CommandSubscriptionsContainer, EventBus, EventSubscriber, - ExecutableCommand, NyxBot, - ParentCommand, + ReadonlyCommandDeployer, TopLevelCommand, } from '@nyx-discord/core'; import { CommandEventEnum, CommandExecutionMeta } from '@nyx-discord/core'; @@ -45,12 +45,13 @@ import { InteractionType } from 'discord.js'; import { BasicEventBus } from '../event/bus/BasicEventBus.js'; import { DefaultCommandCustomIdCodec } from './customId/DefaultCommandCustomIdCodec.js'; +import { DefaultCommandDeployer } from './deploy/DefaultCommandDeployer'; import { DefaultCommandAutocompleteSubscriber } from './events/DefaultCommandAutocompleteSubscriber.js'; import { DefaultCommandInteractionSubscriber } from './events/DefaultCommandInteractionSubscriber.js'; import { DefaultCommandSubscriptionsContainer } from './events/DefaultCommandSubscriptionsContainer.js'; -import { DefaultCommandExecutor } from './executor/DefaultCommandExecutor.js'; +import { DefaultCommandExecutor } from './execution/DefaultCommandExecutor.js'; import { DefaultCommandRepository } from './repository/DefaultCommandRepository.js'; -import { DefaultCommandResolver } from './resolver/DefaultCommandResolver.js'; +import { DefaultCommandResolver } from './resolve/DefaultCommandResolver'; type CommandManagerOptions = { subscriptionsContainer: CommandSubscriptionsContainer; @@ -58,6 +59,7 @@ type CommandManagerOptions = { repository: CommandRepository; executor: CommandExecutor; customIdCodec: CommandCustomIdCodec; + deployer: CommandDeployer; eventBus: EventBus; }; @@ -74,6 +76,8 @@ export class DefaultCommandManager implements CommandManager { protected readonly customIdCodec: CommandCustomIdCodec; + protected readonly deployer: CommandDeployer; + protected readonly eventBus: EventBus; constructor(bot: NyxBot, options: CommandManagerOptions) { @@ -84,13 +88,13 @@ export class DefaultCommandManager implements CommandManager { this.resolver = options.resolver; this.subscriptionsContainer = options.subscriptionsContainer; this.eventBus = options.eventBus; + this.deployer = options.deployer; } public static create( bot: NyxBot, client: Client, clientBus: EventBus, - refreshCommands: boolean, options?: Partial, ): CommandManager { const constructorOptions: Partial = options ?? {}; @@ -105,10 +109,7 @@ export class DefaultCommandManager implements CommandManager { } if (!constructorOptions.repository) { - constructorOptions.repository = DefaultCommandRepository.create( - client, - refreshCommands, - ); + constructorOptions.repository = DefaultCommandRepository.create(); } if (!constructorOptions.executor) { @@ -132,6 +133,10 @@ export class DefaultCommandManager implements CommandManager { ); } + if (!constructorOptions.deployer) { + constructorOptions.deployer = new DefaultCommandDeployer(client); + } + return new DefaultCommandManager( bot, constructorOptions as CommandManagerOptions, @@ -140,57 +145,88 @@ export class DefaultCommandManager implements CommandManager { public async onStart(): Promise { await this.subscriptionsContainer.onStart(); - await this.repository.onStart(); + await this.deployer.start(); } public async onSetup(): Promise { - await this.repository.onSetup(); await this.subscriptionsContainer.onSetup(); await this.eventBus.onRegister(); } public async onStop(): Promise { - await this.repository.onStop(); await this.subscriptionsContainer.onStop(); await this.eventBus.onUnregister(); } - public async addCommand(command: TopLevelCommand): Promise { - await this.repository.addCommand(command); + public async addCommands(...commands: TopLevelCommand[]): Promise { + for (const command of commands) { + this.repository.addCommand(command); + } - Promise.resolve( - this.eventBus.emit(CommandEventEnum.CommandAdd, [command]), - ).catch((error) => { - const id = command.getId(); + try { + await this.deployer.addCommands(...commands); + } catch (error) { + for (const command of commands) { + this.repository.removeCommand(command); + } - this.bot.logger.error( - `'Uncaught bus error while emitting command add '${id}'.`, - error, - ); - }); + throw error; + } + + for (const command of commands) { + Promise.resolve( + this.eventBus.emit(CommandEventEnum.CommandAdd, [command]), + ).catch((error) => { + const id = command.getId(); + + this.bot.logger.error( + `'Uncaught bus error while emitting command add '${id}'.`, + error, + ); + }); + } return this; } - public async removeCommand(command: TopLevelCommand): Promise { - await this.repository.removeCommand(command); + public async removeCommands(...commands: TopLevelCommand[]): Promise { + for (const command of commands) { + this.repository.removeCommand(command); + } - Promise.resolve( - this.eventBus.emit(CommandEventEnum.CommandRemove, [command]), - ).catch((error) => { - const id = command.getId(); + try { + await this.deployer.removeCommands(...commands); + } catch (error) { + for (const command of commands) { + this.repository.addCommand(command); + } - this.bot.logger.error( - `'Uncaught bus error while emitting command remove '${id}'.`, - error, - ); - }); + throw error; + } + + for (const command of commands) { + Promise.resolve( + this.eventBus.emit(CommandEventEnum.CommandRemove, [command]), + ).catch((error) => { + const id = command.getId(); + + this.bot.logger.error( + `'Uncaught bus error while emitting command remove '${id}'.`, + error, + ); + }); + } return this; } - public async updateParentCommand(command: ParentCommand): Promise { - await this.repository.updateParentCommand(command); + public async editCommands(...commands: TopLevelCommand[]): Promise { + for (const command of commands) { + this.repository.removeCommand(command); + this.repository.addCommand(command); + } + + await this.deployer.editCommands(...commands); return this; } @@ -199,11 +235,10 @@ export class DefaultCommandManager implements CommandManager { interaction: AutocompleteInteraction, meta?: CommandExecutionMeta, ): Promise { - const resolvedCommandData = - this.resolver.resolveFromAutocompleteInteraction(interaction); - if (!resolvedCommandData) return false; - - const command = this.repository.locateByData(resolvedCommandData); + const command = this.resolver.resolveFromAutocompleteInteraction( + interaction, + this.repository, + ); if (!command) return false; const metadata = @@ -215,6 +250,7 @@ export class DefaultCommandManager implements CommandManager { ); try { + if (!command.isSubCommand() && !command.isStandalone()) return false; await this.executor.autocomplete(command, interaction, metadata); } catch (error) { const executionId = String(metadata.getId()); @@ -247,22 +283,28 @@ export class DefaultCommandManager implements CommandManager { interaction: CommandExecutableInteraction, meta?: CommandExecutionMeta, ): Promise { - let command: ExecutableCommand | null; + let command: AnyExecutableCommand | null; if (interaction.type === InteractionType.ApplicationCommand) { - const data = this.resolver.resolveFromCommandInteraction(interaction); - if (!data) return false; - command = this.repository.locateByData(data); + command = this.resolver.resolveFromCommandInteraction( + interaction, + this.repository, + ); } else { const { customId } = interaction; - const commandData = this.customIdCodec.deserializeToData(customId); - if (!commandData) return false; + const commandName = this.customIdCodec.deserializeToObjectId(customId); + if (!commandName) return false; - const foundCommand = this.repository.locateByData(commandData); - if (!foundCommand || !foundCommand.isExecutable()) return false; + const names = commandName.split( + this.customIdCodec.getNamesSeparator(), + ) as [string, ...string[]]; + const found = this.repository.locateByNameTree(...names); + if (!found || found.isParent() || found.isSubCommandGroup()) { + return false; + } - command = foundCommand; + command = found; } if (!command) return false; @@ -331,4 +373,8 @@ export class DefaultCommandManager implements CommandManager { public getEventBus(): EventBus { return this.eventBus; } + + public getDeployer(): ReadonlyCommandDeployer { + return this.deployer; + } } diff --git a/packages/framework/src/features/command/commands/abstract/AbstractCommand.ts b/packages/framework/src/features/command/commands/AbstractCommand.ts similarity index 71% rename from packages/framework/src/features/command/commands/abstract/AbstractCommand.ts rename to packages/framework/src/features/command/commands/AbstractCommand.ts index befdb26e..b13d46ee 100644 --- a/packages/framework/src/features/command/commands/abstract/AbstractCommand.ts +++ b/packages/framework/src/features/command/commands/AbstractCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,9 +25,7 @@ import { Collection } from '@discordjs/collection'; import type { Command, - CommandData, - CommandVisitor, - ExecutableCommand, + ContextMenuCommand, MetaCollection, ParentCommand, ReadonlyMetaCollection, @@ -36,18 +34,14 @@ import type { SubCommandGroup, } from '@nyx-discord/core'; -export abstract class AbstractCommand - implements Command -{ +export abstract class AbstractCommand implements Command { protected readonly meta: MetaCollection = new Collection(); - protected abstract data: Data; - - public isStandaloneCommand(): this is StandaloneCommand { + public isStandalone(): this is StandaloneCommand { return false; } - public isParentCommand(): this is ParentCommand { + public isParent(): this is ParentCommand { return false; } @@ -59,29 +53,21 @@ export abstract class AbstractCommand return false; } - public isExecutable(): this is ExecutableCommand { + public isContextMenu(): this is ContextMenuCommand { return false; } - public getData(): Readonly { - return this.data; - } - public getMeta(): ReadonlyMetaCollection { return this.meta; } - public getReadableName(): string { - return this.data.name; + public toString(): string { + return this.getName(); } - public getId(): string { - return this.getReadableName(); - } + public abstract getNameTree(): ReadonlyArray; - public toString(): string { - return this.getReadableName(); - } + public abstract getData(): Data; - public abstract acceptVisitor(visitor: CommandVisitor): void; + public abstract getName(): string; } diff --git a/packages/framework/src/features/command/commands/AbstractContextMenuCommand.ts b/packages/framework/src/features/command/commands/AbstractContextMenuCommand.ts new file mode 100644 index 00000000..d7374739 --- /dev/null +++ b/packages/framework/src/features/command/commands/AbstractContextMenuCommand.ts @@ -0,0 +1,99 @@ +/* + * MIT License + * + * Copyright (c) 2024 Amgelo563 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { + CommandExecutionMeta, + ContextMenuCommand, +} from '@nyx-discord/core'; +import type { + Awaitable, + ContextMenuCommandBuilder, + MessageContextMenuCommandInteraction, + Snowflake, + UserContextMenuCommandInteraction, +} from 'discord.js'; + +import { NotImplementedError } from '../../../errors/NotImplementedError'; +import { AbstractExecutableCommand } from './executable/AbstractExecutableCommand'; + +export abstract class AbstractContextMenuCommand + extends AbstractExecutableCommand< + ReturnType, + MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction + > + implements ContextMenuCommand +{ + public execute( + interaction: + | MessageContextMenuCommandInteraction + | UserContextMenuCommandInteraction, + metadata: CommandExecutionMeta, + ): Awaitable { + if (interaction.isMessageContextMenuCommand()) { + return this.executeMessage(interaction, metadata); + } else if (interaction.isUserContextMenuCommand()) { + return this.executeUser(interaction, metadata); + } + } + + public getName(): string { + return this.createData().name; + } + + public getData(): ReturnType { + return this.createData().toJSON(); + } + + public getGuilds(): ReadonlyArray | null { + return null; + } + + public getId(): string { + return this.createData().name; + } + + public override isContextMenu(): this is ContextMenuCommand { + return true; + } + + public getNameTree(): ReadonlyArray { + return [this.getName()]; + } + + protected abstract createData(): ContextMenuCommandBuilder; + + protected executeUser( + _interaction: UserContextMenuCommandInteraction, + _metadata: CommandExecutionMeta, + ): Awaitable { + throw new NotImplementedError(); + } + + protected executeMessage( + _interaction: MessageContextMenuCommandInteraction, + _metadata: CommandExecutionMeta, + ): Awaitable { + throw new NotImplementedError(); + } +} diff --git a/packages/framework/src/features/command/commands/AbstractParentCommand.ts b/packages/framework/src/features/command/commands/AbstractParentCommand.ts index e43c5816..4f647177 100644 --- a/packages/framework/src/features/command/commands/AbstractParentCommand.ts +++ b/packages/framework/src/features/command/commands/AbstractParentCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,35 +23,73 @@ */ import type { - CommandVisitor, ParentCommand, - ParentCommandData, SubCommand, SubCommandGroup, } from '@nyx-discord/core'; -import { AbstractChildableCommand } from './abstract/AbstractChildableCommand.js'; +import { AssertionError } from '@nyx-discord/core'; +import type { SlashCommandSubcommandsOnlyBuilder, Snowflake } from 'discord.js'; + +import { AbstractChildableCommand } from './child/AbstractChildableCommand'; -/** - * A top level command that serves only to contain - * {@link AbstractSubCommand subcommands} or - * {@link AbstractSubCommandGroup subcommand groups}. - * - * Parent commands cannot be executed by themselves. - */ export abstract class AbstractParentCommand extends AbstractChildableCommand< - ParentCommandData, + ReturnType, SubCommand | SubCommandGroup > implements ParentCommand { protected override readonly childLimit = 25; - public override acceptVisitor(visitor: CommandVisitor): void { - visitor.visitParentCommand(this); + public override isParent(): this is ParentCommand { + return true; } - public override isParentCommand(): this is ParentCommand { - return true; + public getData(): ReturnType { + const data = this.createData(); + if (data.options.length) { + throw new AssertionError('ParentCommands cannot set their options.'); + } + + for (const child of this.children.values()) { + if (child.isSubCommand()) { + data.addSubcommand(child.getData()); + } else if (child.isSubCommandGroup()) { + data.addSubcommandGroup(() => { + const builder = child.getData(); + if (builder.options.length) { + throw new AssertionError( + 'SubCommandGroups cannot set their options.', + ); + } + + for (const subCommand of child.getChildren().values()) { + builder.addSubcommand(subCommand.getData()); + } + + return builder; + }); + } + } + + return data.toJSON(); } + + public getId(): string { + return this.createData().name; + } + + public getGuilds(): ReadonlyArray | null { + return null; + } + + public getName(): string { + return this.createData().name; + } + + public getNameTree(): ReadonlyArray { + return [this.getName()]; + } + + protected abstract createData(): SlashCommandSubcommandsOnlyBuilder; } diff --git a/packages/framework/src/features/command/commands/AbstractStandaloneCommand.ts b/packages/framework/src/features/command/commands/AbstractStandaloneCommand.ts index 0f0cd41d..73380801 100644 --- a/packages/framework/src/features/command/commands/AbstractStandaloneCommand.ts +++ b/packages/framework/src/features/command/commands/AbstractStandaloneCommand.ts @@ -24,85 +24,54 @@ import type { CommandExecutionMeta, - CommandVisitor, StandaloneCommand, - StandaloneCommandData, - StandaloneCommandReferenceData, } from '@nyx-discord/core'; -import { ResolvedCommandType } from '@nyx-discord/core'; import type { + AutocompleteInteraction, Awaitable, - MessageContextMenuCommandInteraction, - UserContextMenuCommandInteraction, + ChatInputCommandInteraction, + SlashCommandBuilder, + SlashCommandOptionsOnlyBuilder, + Snowflake, } from 'discord.js'; -import { ApplicationCommandType } from 'discord.js'; -import { NotImplementedError } from '../../../errors/NotImplementedError.js'; -import { AbstractExecutableCommand } from './abstract/AbstractExecutableCommand.js'; +import { AbstractExecutableCommand } from './executable/AbstractExecutableCommand'; export abstract class AbstractStandaloneCommand - extends AbstractExecutableCommand + extends AbstractExecutableCommand< + ReturnType, + ChatInputCommandInteraction + > implements StandaloneCommand { - public static readonly DefaultContextData: ReadonlyArray = - [ApplicationCommandType.ChatInput]; - - protected contexts: ApplicationCommandType[] = [ - ...AbstractStandaloneCommand.DefaultContextData, - ]; - - public getContexts(): ReadonlyArray { - return this.contexts; - } - - public override acceptVisitor(visitor: CommandVisitor): void { - visitor.visitStandaloneCommand(this); - } - - public override isStandaloneCommand(): this is StandaloneCommand { - return true; + public getName(): string { + return this.createData().name; } - public executeUser( - _interaction: UserContextMenuCommandInteraction, - _metadata: CommandExecutionMeta, - ): Awaitable { - throw new NotImplementedError(); + public getData(): ReturnType { + return this.createData().toJSON(); } - public executeMessage( - _interaction: MessageContextMenuCommandInteraction, - _metadata: CommandExecutionMeta, - ): Awaitable { - throw new NotImplementedError(); + public getGuilds(): ReadonlyArray | null { + return null; } - public override getId(): string { - const { name } = this.data; - const contextBitmask = this.generateContextBitmask(); - return `${name}/${contextBitmask}`; + public getId(): string { + return this.createData().name; } - public override getData(): Readonly { - return this.data; + public override isStandalone(): this is StandaloneCommand { + return true; } - /** Generates a bitmask for the command's {@link contexts} data. */ - protected generateContextBitmask(): string { - let bitmask = 0; + public abstract autocomplete( + interaction: AutocompleteInteraction, + metadata: CommandExecutionMeta, + ): Awaitable; - for (const value of this.contexts) { - bitmask |= value; - } - - return bitmask.toString(10); + public getNameTree(): ReadonlyArray { + return [this.getName()]; } - public toReferenceData(): StandaloneCommandReferenceData { - return { - type: ResolvedCommandType.StandaloneCommand, - root: this.data.name, - commandType: this.contexts[0], - }; - } + protected abstract createData(): SlashCommandOptionsOnlyBuilder; } diff --git a/packages/framework/src/features/command/commands/AbstractSubCommand.ts b/packages/framework/src/features/command/commands/AbstractSubCommand.ts index f562164c..20033aee 100644 --- a/packages/framework/src/features/command/commands/AbstractSubCommand.ts +++ b/packages/framework/src/features/command/commands/AbstractSubCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,20 +23,27 @@ */ import type { - CommandReferenceData, - CommandVisitor, + CommandExecutionMeta, ParentCommand, SubCommand, - SubCommandData, SubCommandGroup, } from '@nyx-discord/core'; -import { ResolvedCommandType } from '@nyx-discord/core'; +import type { + AutocompleteInteraction, + Awaitable, + ChatInputCommandInteraction, + SlashCommandSubcommandBuilder, +} from 'discord.js'; +import { NotImplementedError } from '../../../errors/NotImplementedError'; -import { AbstractExecutableCommand } from './abstract/AbstractExecutableCommand.js'; +import { AbstractExecutableCommand } from './executable/AbstractExecutableCommand'; /** A child, executable command that belongs to an {@link ParentCommand} or {@link SubCommandGroup}. */ export abstract class AbstractSubCommand - extends AbstractExecutableCommand + extends AbstractExecutableCommand< + SlashCommandSubcommandBuilder, + ChatInputCommandInteraction + > implements SubCommand { protected readonly parent: ParentCommand | SubCommandGroup; @@ -46,39 +53,32 @@ export abstract class AbstractSubCommand this.parent = parent; } - public getParent(): ParentCommand | SubCommandGroup { - return this.parent; + public getName(): string { + return this.createData().name; } - public override acceptVisitor(visitor: CommandVisitor): void { - visitor.visitSubCommand(this); + public getParent(): ParentCommand | SubCommandGroup { + return this.parent; } public override isSubCommand(): this is SubCommand { return true; } - public override getReadableName(): string { - const names: string[] = [this.parent.getReadableName(), this.data.name]; - return names.join('/'); + public autocomplete( + _interaction: AutocompleteInteraction, + _metadata: CommandExecutionMeta, + ): Awaitable { + throw new NotImplementedError(); } - public toReferenceData(): CommandReferenceData { - if (this.parent.isParentCommand()) { - return { - type: ResolvedCommandType.SubCommand, - root: this.parent.getData().name, - subCommand: this.data.name, - }; - } else { - const root = this.parent.getParent(); + public getData(): SlashCommandSubcommandBuilder { + return this.createData(); + } - return { - type: ResolvedCommandType.SubCommandOnGroup, - root: root.getData().name, - group: this.parent.getData().name, - subCommand: this.data.name, - }; - } + public getNameTree(): ReadonlyArray { + return this.parent.getNameTree().concat(this.getName()); } + + protected abstract createData(): SlashCommandSubcommandBuilder; } diff --git a/packages/framework/src/features/command/commands/AbstractSubCommandGroup.ts b/packages/framework/src/features/command/commands/AbstractSubCommandGroup.ts index 6a423640..ccb7f464 100644 --- a/packages/framework/src/features/command/commands/AbstractSubCommandGroup.ts +++ b/packages/framework/src/features/command/commands/AbstractSubCommandGroup.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,16 +23,18 @@ */ import type { - CommandVisitor, ParentCommand, SubCommand, SubCommandGroup, - SubCommandGroupData, } from '@nyx-discord/core'; -import { AbstractChildableCommand } from './abstract/AbstractChildableCommand.js'; +import type { SlashCommandSubcommandGroupBuilder } from 'discord.js'; +import { AbstractChildableCommand } from './child/AbstractChildableCommand'; export abstract class AbstractSubCommandGroup - extends AbstractChildableCommand + extends AbstractChildableCommand< + SlashCommandSubcommandGroupBuilder, + SubCommand + > implements SubCommandGroup { protected override readonly childLimit = 25; @@ -44,20 +46,25 @@ export abstract class AbstractSubCommandGroup this.parent = parent; } - public override isSubCommandGroup(): this is SubCommandGroup { - return true; + public getName(): string { + return this.createData().name; } - public override acceptVisitor(visitor: CommandVisitor) { - visitor.visitSubCommandGroup(this); + public override isSubCommandGroup(): this is SubCommandGroup { + return true; } public getParent(): ParentCommand { return this.parent; } - public override getReadableName(): string { - const names: string[] = [this.parent.getReadableName(), this.data.name]; - return names.join('/'); + public getData(): SlashCommandSubcommandGroupBuilder { + return this.createData(); } + + public getNameTree(): ReadonlyArray { + return this.parent.getNameTree().concat(this.getName()); + } + + protected abstract createData(): SlashCommandSubcommandGroupBuilder; } diff --git a/packages/framework/src/features/command/commands/abstract/AbstractChildableCommand.ts b/packages/framework/src/features/command/commands/child/AbstractChildableCommand.ts similarity index 62% rename from packages/framework/src/features/command/commands/abstract/AbstractChildableCommand.ts rename to packages/framework/src/features/command/commands/child/AbstractChildableCommand.ts index 24497e7a..c90a7be3 100644 --- a/packages/framework/src/features/command/commands/abstract/AbstractChildableCommand.ts +++ b/packages/framework/src/features/command/commands/child/AbstractChildableCommand.ts @@ -22,68 +22,79 @@ * SOFTWARE. */ +import type { ReadonlyCollection } from '@discordjs/collection'; +import { Collection } from '@discordjs/collection'; import type { ChildableCommand, ChildCommand, ClassImplements, - CommandData, } from '@nyx-discord/core'; -import { AssertionError, ObjectNotFoundError } from '@nyx-discord/core'; +import { + AssertionError, + IllegalDuplicateError, + ObjectNotFoundError, +} from '@nyx-discord/core'; -import { AbstractCommand } from './AbstractCommand.js'; +import { AbstractCommand } from '../AbstractCommand'; export abstract class AbstractChildableCommand< - Data extends CommandData, - Child extends ChildCommand>, + Data, + Child extends ChildCommand, > extends AbstractCommand implements ChildableCommand { protected abstract readonly childLimit: number; - protected abstract readonly children: Child[]; + protected readonly children: Collection = new Collection(); public get size() { - return this.children.length; + return this.children.size; } - public addChild(child: Child): this { - if (this.children.length + 1 > this.childLimit) { - throw new RangeError( - `Can't add child. Children limit of ${this.childLimit}`, - ); - } + public addChildren(...children: Child[]): this { + for (const child of children) { + if (this.children.size + 1 > this.childLimit) { + throw new RangeError( + `Can't add child. Children limit of ${this.childLimit}`, + ); + } - if (child.getParent() !== this) { - throw new AssertionError(); - } + if (child.getParent() !== this) { + throw new AssertionError(); + } + const name = child.getName(); - this.children.push(child); - return this; - } + const duplicate = this.children.get(name); + if (duplicate) { + throw new IllegalDuplicateError(duplicate, child); + } - public addChildren(children: Child[]): this { - children.forEach((child) => this.addChild(child)); + this.children.set(name, child); + } return this; } public removeChildByName(child: string): this { - const childToRemove = this.findChildByName(child); + const childToRemove = this.children.get(child); if (!childToRemove) { throw new ObjectNotFoundError(); } - return this.removeChildByInstance(childToRemove); + this.children.delete(child); + + return this; } public removeChildByInstance(child: Child) { - if (this.children.length === 1) { + if (this.children.size === 1) { throw new RangeError(); } - const index = this.children.indexOf(child); - if (index === -1) { + const key = this.children.findKey((storedChild) => storedChild === child); + if (!key) { throw new ObjectNotFoundError(); } - this.children.splice(index, 1); + + this.children.delete(key); return this; } @@ -96,18 +107,15 @@ export abstract class AbstractChildableCommand< return foundChild ?? null; } - public getMaxChildren(): number { - return this.childLimit; + public findChildByName(name: string): Child | null { + return this.children.get(name) ?? null; } - /** Find a subcommand belonging to this group by its name. */ - public findChildByName(name: string): Child | null { - return ( - this.children.find((children) => children.getData().name === name) ?? null - ); + public getMaxChildren(): number { + return this.childLimit; } - public getChildren(): ReadonlyArray { + public getChildren(): ReadonlyCollection { return this.children; } } diff --git a/packages/framework/src/features/command/commands/abstract/AbstractExecutableCommand.ts b/packages/framework/src/features/command/commands/executable/AbstractExecutableCommand.ts similarity index 67% rename from packages/framework/src/features/command/commands/abstract/AbstractExecutableCommand.ts rename to packages/framework/src/features/command/commands/executable/AbstractExecutableCommand.ts index 694eecc0..eb97b1d3 100644 --- a/packages/framework/src/features/command/commands/abstract/AbstractExecutableCommand.ts +++ b/packages/framework/src/features/command/commands/executable/AbstractExecutableCommand.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,11 +23,9 @@ */ import type { - CommandData, + ApplicationCommandInteraction, CommandExecutionMeta, CommandFilter, - CommandOptionData, - CommandReferenceData, ComponentCommandInteraction, CustomIdBuilder, ExecutableCommand, @@ -35,46 +33,27 @@ import type { } from '@nyx-discord/core'; import type { AnySelectMenuInteraction, - ApplicationCommandOptionChoiceData, - AutocompleteFocusedOption, - AutocompleteInteraction, Awaitable, ButtonInteraction, - ChatInputCommandInteraction, ModalSubmitInteraction, } from 'discord.js'; -import { NotImplementedError } from '../../../../errors/NotImplementedError.js'; -import { AbstractCommand } from './AbstractCommand.js'; +import { NotImplementedError } from '../../../../errors/NotImplementedError'; +import { AbstractCommand } from '../AbstractCommand'; -/** A command that can be executed (called) by an interaction. */ -export abstract class AbstractExecutableCommand +export abstract class AbstractExecutableCommand< + Data, + Interaction extends ApplicationCommandInteraction, + > extends AbstractCommand - implements ExecutableCommand + implements ExecutableCommand { - protected readonly filter: CommandFilter | null = null; + protected readonly filter: CommandFilter | null = null; - protected options: CommandOptionData[] = []; - - public getOptions(): ReadonlyArray { - return this.options; - } - - public autocomplete( - _option: AutocompleteFocusedOption, - _interaction: AutocompleteInteraction, - _metadata: CommandExecutionMeta, - _respond: ( - options: ApplicationCommandOptionChoiceData[], - ) => Awaitable, - ): Awaitable { - return []; - } - - public async handleInteraction( + public handleInteraction( interaction: ComponentCommandInteraction, metadata: CommandExecutionMeta, - ): Promise { + ): Awaitable { if (interaction.isButton()) return this.handleButton(interaction, metadata); if (interaction.isModalSubmit()) { return this.handleModal(interaction, metadata); @@ -82,13 +61,14 @@ export abstract class AbstractExecutableCommand return this.handleSelectMenu(interaction, metadata); } - public getFilter(): CommandFilter | null { + public getFilter(): CommandFilter | null { return this.filter; } - public override isExecutable(): this is ExecutableCommand { - return true; - } + public abstract execute( + interaction: Interaction, + metadata: CommandExecutionMeta, + ): Awaitable; protected handleButton( _interaction: ButtonInteraction, @@ -118,11 +98,4 @@ export abstract class AbstractExecutableCommand protected getCustomId(bot: NyxBot): string { return bot.commands.getCustomIdCodec().serializeToCustomId(this); } - - public abstract execute( - interaction: ChatInputCommandInteraction, - metadata: CommandExecutionMeta, - ): Awaitable; - - public abstract toReferenceData(): CommandReferenceData; } diff --git a/packages/framework/src/features/command/customId/DefaultCommandCustomIdCodec.ts b/packages/framework/src/features/command/customId/DefaultCommandCustomIdCodec.ts index 3a84fb0b..fc24b857 100644 --- a/packages/framework/src/features/command/customId/DefaultCommandCustomIdCodec.ts +++ b/packages/framework/src/features/command/customId/DefaultCommandCustomIdCodec.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,54 +23,42 @@ */ import type { + AnyExecutableCommand, CommandCustomIdCodec, - CommandData, - CommandReferenceData, - ExecutableCommand, } from '@nyx-discord/core'; -import { CommandCustomIdBuilder } from '@nyx-discord/core'; -import { BasicCustomIdCodec } from '../../../customId/BasicCustomIdCodec.js'; + +import { AbstractCustomIdCodec } from '../../../customId/AbstractCustomIdCodec'; export class DefaultCommandCustomIdCodec - extends BasicCustomIdCodec> + extends AbstractCustomIdCodec implements CommandCustomIdCodec { public static readonly DefaultNamespace = 'CMD'; + public static readonly DefaultNamesSeparator = ':'; + + protected readonly namesSeparator: string; + constructor( prefix: string = DefaultCommandCustomIdCodec.DefaultNamespace, separator: string = DefaultCommandCustomIdCodec.DefaultSeparator, dataSeparator: string = DefaultCommandCustomIdCodec.DefaultDataSeparator, + namesSeparator: string = DefaultCommandCustomIdCodec.DefaultNamesSeparator, ) { super(prefix, separator, dataSeparator); + + this.namesSeparator = namesSeparator; } public static create(): CommandCustomIdCodec { return new DefaultCommandCustomIdCodec(); } - public override createCustomIdBuilder( - command: ExecutableCommand, - ) { - const id = command.getId(); - const data = command.toReferenceData(); - - return new CommandCustomIdBuilder({ - data, - namespace: this.namespace, - objectId: id, - dataSeparator: this.dataSeparator, - separator: this.separator, - }); + public getNamesSeparator(): string { + return this.namesSeparator; } - public deserializeToData(customId: string): CommandReferenceData | null { - return ( - CommandCustomIdBuilder.fromCommandCustomId( - customId, - this.separator, - this.dataSeparator, - )?.getReferenceData() ?? null - ); + protected getIdOf(serialized: AnyExecutableCommand): string { + return serialized.getNameTree().join(this.namesSeparator); } } diff --git a/packages/framework/src/features/command/deploy/DefaultCommandDeployer.ts b/packages/framework/src/features/command/deploy/DefaultCommandDeployer.ts new file mode 100644 index 00000000..4e71eb5b --- /dev/null +++ b/packages/framework/src/features/command/deploy/DefaultCommandDeployer.ts @@ -0,0 +1,219 @@ +/* + * MIT License + * + * Copyright (c) 2024 Amgelo563 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { ReadonlyCollection } from '@discordjs/collection'; +import { Collection } from '@discordjs/collection'; +import type { CommandDeployer, TopLevelCommand } from '@nyx-discord/core'; +import { + AssertionError, + IllegalStateError, + ObjectNotFoundError, +} from '@nyx-discord/core'; +import type { ApplicationCommand, Client, Snowflake } from 'discord.js'; + +export class DefaultCommandDeployer implements CommandDeployer { + protected readonly client: Client; + + protected readonly mappings: Collection = + new Collection(); + + // Array while unstarted, null if started + protected pendingAdd: TopLevelCommand[] | null = []; + + constructor(client: Client) { + this.client = client; + } + + public async start(): Promise { + if (this.pendingAdd === null) { + throw new IllegalStateError('Already started.'); + } + + await this.deploy(...this.pendingAdd); + this.pendingAdd = null; + + return this; + } + + public async addCommands(...commands: TopLevelCommand[]): Promise { + if (this.pendingAdd === null) { + await this.deploy(...commands); + } else { + this.pendingAdd.push(...commands); + } + + return this; + } + + public async removeCommands(...commands: TopLevelCommand[]): Promise { + if (this.pendingAdd === null) { + await this.unDeploy(...commands); + } else { + for (const command of commands) { + const index = this.pendingAdd.indexOf(command); + if (index === -1) { + throw new AssertionError('Command not found in pending add.'); + } + + this.pendingAdd.splice(index, 1); + } + } + + return this; + } + + public async editCommands(...commands: TopLevelCommand[]): Promise { + const applicationManager = this.client.application?.commands; + if (!applicationManager) { + throw new IllegalStateError('Cannot update commands while not started.'); + } + + for (const command of commands) { + const id = command.getId(); + const applicationCommand = this.mappings.get(id); + if (!applicationCommand) { + throw new ObjectNotFoundError(`Command application not found: ${id}.`); + } + + const newMapping = await applicationManager.edit( + applicationCommand, + command.getData(), + ); + + this.mappings.set(id, newMapping); + } + + return this; + } + + public getMappings(): ReadonlyCollection { + return this.mappings; + } + + public *keys(): IterableIterator { + yield* this.mappings.keys(); + } + + public *values(): IterableIterator { + yield* this.mappings.values(); + } + + public *entries(): IterableIterator<[string, ApplicationCommand]> { + yield* this.mappings.entries(); + } + + public next(): IteratorResult { + return this.values().next(); + } + + public [Symbol.iterator](): IterableIterator { + return this.values(); + } + + protected async unDeploy(...commands: TopLevelCommand[]): Promise { + const applicationManager = this.client.application?.commands; + if (!applicationManager) { + throw new IllegalStateError('Cannot update commands while not started.'); + } + + for (const command of commands) { + const id = command.getId(); + const mapping = this.mappings.get(id); + if (!mapping) { + throw new ObjectNotFoundError(`Command not found: ${id}.`); + } + + await applicationManager.delete(mapping); + this.mappings.delete(id); + } + } + + protected async deploy(...commands: TopLevelCommand[]): Promise { + const applicationManager = this.client.application?.commands; + if (!applicationManager) { + throw new IllegalStateError('Cannot update commands while not started.'); + } + + // Aggregate to Collection to minimize API calls + const guildAggregate = new Collection< + Snowflake | null, + TopLevelCommand[] + >(); + for (const command of commands) { + const possibleDuplicate = this.mappings.get(command.getId()); + if (possibleDuplicate) { + throw new AssertionError( + `Already deployed command ${command}, found duplicate: '${possibleDuplicate}'.`, + ); + } + + const guilds = command.getGuilds(); + for (const guild of guilds ?? [null]) { + const guildCommands = guildAggregate.get(guild) ?? []; + guildCommands.push(command); + guildAggregate.set(guild, guildCommands); + } + } + + const deployPromises: Promise[] = []; + for (const [guild, guildCommands] of guildAggregate) { + const promise = Promise.resolve().then(async () => { + const datas = guildCommands.map((command) => command.getData()); + + const result = + guild === null + ? await applicationManager.set(datas) + : await applicationManager.set(datas, guild); + + this.mapCommands(result.values(), guildCommands); + }); + + deployPromises.push(promise); + } + + await Promise.all(deployPromises); + } + + protected mapCommands( + applications: IterableIterator, + commands: TopLevelCommand[], + ): void { + for (const application of applications) { + const name = application.name; + const command = commands.find((command) => { + const data = command.getData(); + return data.name === name && data.type === application.type; + }); + + if (!command) { + throw new ObjectNotFoundError( + `Could not associate any command in '${commands}' with an application of type '${application.type}' and name '${name}'.`, + ); + } + + // We assume duplicates have already been checked + this.mappings.set(command.getId(), application); + } + } +} diff --git a/packages/framework/src/features/command/executor/DefaultCommandExecutor.ts b/packages/framework/src/features/command/execution/DefaultCommandExecutor.ts similarity index 78% rename from packages/framework/src/features/command/executor/DefaultCommandExecutor.ts rename to packages/framework/src/features/command/execution/DefaultCommandExecutor.ts index 619ab6e5..037bc415 100644 --- a/packages/framework/src/features/command/executor/DefaultCommandExecutor.ts +++ b/packages/framework/src/features/command/execution/DefaultCommandExecutor.ts @@ -23,7 +23,8 @@ */ import type { - CommandData, + AnyExecutableCommand, + ChatExecutableCommand, CommandError, CommandErrorHandler, CommandExecutableInteraction, @@ -32,9 +33,8 @@ import type { CommandExecutor, CommandMiddleware, ComponentCommandInteraction, - ExecutableCommand, + ContextMenuCommand, MiddlewareList, - StandaloneCommand, } from '@nyx-discord/core'; import { CommandAutocompleteError, @@ -69,25 +69,20 @@ export class DefaultCommandExecutor implements CommandExecutor { public static create(): CommandExecutor { return new DefaultCommandExecutor( - BasicErrorHandler.create< - ExecutableCommand, - CommandExecutionArgs - >(), + BasicErrorHandler.create(), CommandMiddlewareList.create(), ); } public async execute( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, metadata: CommandExecutionMeta, ): Promise { - if (interaction.isAutocomplete()) { - await this.autocomplete(command, interaction, metadata); - return true; - } - - if (interaction.isChatInputCommand()) { + if ( + interaction.isChatInputCommand() + && (command.isSubCommand() || command.isStandalone()) + ) { await this.executeChatInput(command, interaction, metadata); return true; } @@ -98,14 +93,14 @@ export class DefaultCommandExecutor implements CommandExecutor { } if (interaction.isUserContextMenuCommand()) { - if (!command.isStandaloneCommand()) return false; + if (!command.isContextMenu()) return false; await this.executeUser(command, interaction, metadata); return true; } if (interaction.isMessageContextMenuCommand()) { - if (!command.isStandaloneCommand()) return false; + if (!command.isContextMenu()) return false; await this.executeMessage(command, interaction, metadata); return true; @@ -115,43 +110,14 @@ export class DefaultCommandExecutor implements CommandExecutor { } public async autocomplete( - command: ExecutableCommand, + command: ChatExecutableCommand, interaction: AutocompleteInteraction, metadata: CommandExecutionMeta, ): Promise { - const focused = interaction.options.getFocused(true); const bot = metadata.getBot(); - const respond = async (options: ApplicationCommandOptionChoiceData[]) => { - try { - await interaction.respond( - options as ApplicationCommandOptionChoiceData[], - ); - } catch (error) { - const wrappedError = this.wrapAutocompleteError( - error as Error, - command, - interaction, - metadata, - ); - - await this.errorHandler.handle( - wrappedError, - command, - [interaction, metadata], - bot, - ); - } - }; - - let choices; try { - choices = await command.autocomplete( - focused, - interaction, - metadata, - respond, - ); + await command.autocomplete(interaction, metadata); } catch (error) { const wrappedError = this.wrapAutocompleteError( error as Error, @@ -167,31 +133,10 @@ export class DefaultCommandExecutor implements CommandExecutor { ); return; } - if (interaction.responded) return; - - const filteredChoices = this.filterChoices([...choices], interaction); - - try { - await respond(filteredChoices); - } catch (error) { - const wrappedError = this.wrapAutocompleteRespondError( - error as Error, - command, - interaction, - metadata, - ); - - await this.errorHandler.handle( - wrappedError, - command, - [interaction, metadata], - bot, - ); - } } public async executeChatInput( - command: ExecutableCommand, + command: ChatExecutableCommand, interaction: ChatInputCommandInteraction, metadata: CommandExecutionMeta, ): Promise { @@ -204,7 +149,7 @@ export class DefaultCommandExecutor implements CommandExecutor { } public async executeComponent( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: ComponentCommandInteraction, metadata: CommandExecutionMeta, ): Promise { @@ -217,7 +162,7 @@ export class DefaultCommandExecutor implements CommandExecutor { } public async executeMessage( - command: StandaloneCommand, + command: ContextMenuCommand, interaction: MessageContextMenuCommandInteraction, metadata: CommandExecutionMeta, ): Promise { @@ -225,12 +170,12 @@ export class DefaultCommandExecutor implements CommandExecutor { command, interaction, metadata, - command.executeMessage, + command.execute, ); } public async executeUser( - command: StandaloneCommand, + command: ContextMenuCommand, interaction: UserContextMenuCommandInteraction, metadata: CommandExecutionMeta, ): Promise { @@ -238,7 +183,7 @@ export class DefaultCommandExecutor implements CommandExecutor { command, interaction, metadata, - command.executeUser, + command.execute, ); } @@ -253,7 +198,7 @@ export class DefaultCommandExecutor implements CommandExecutor { protected async executeCommandMethod< PassedInteraction extends CommandExecutableInteraction, >( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: PassedInteraction, metadata: CommandExecutionMeta, method: ( @@ -284,7 +229,7 @@ export class DefaultCommandExecutor implements CommandExecutor { } protected async checkMiddleware( - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, meta: CommandExecutionMeta, ): Promise { @@ -314,7 +259,7 @@ export class DefaultCommandExecutor implements CommandExecutor { protected wrapMiddlewareError( error: Error, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: CommandExecutableInteraction, meta: CommandExecutionMeta, ): CommandError { @@ -333,7 +278,7 @@ export class DefaultCommandExecutor implements CommandExecutor { protected wrapAutocompleteError( error: Error, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: AutocompleteInteraction, meta: CommandExecutionMeta, ): CommandError { @@ -342,7 +287,7 @@ export class DefaultCommandExecutor implements CommandExecutor { protected wrapAutocompleteRespondError( error: Error, - command: ExecutableCommand, + command: AnyExecutableCommand, interaction: AutocompleteInteraction, meta: CommandExecutionMeta, ): CommandError { diff --git a/packages/framework/src/features/command/filter/AbstractCommandFilter.ts b/packages/framework/src/features/command/filter/AbstractCommandFilter.ts index efaeae78..d8ebea7a 100644 --- a/packages/framework/src/features/command/filter/AbstractCommandFilter.ts +++ b/packages/framework/src/features/command/filter/AbstractCommandFilter.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,15 +23,14 @@ */ import type { - CommandData, + AnyExecutableCommand, CommandExecutionArgs, CommandFilter, - ExecutableCommand, } from '@nyx-discord/core'; import { AbstractFilter } from '../../../filter/AbstractFilter.js'; /** A {@link AbstractFilter Filter} for filtering Command executions. */ export abstract class AbstractCommandFilter - extends AbstractFilter, CommandExecutionArgs> - implements CommandFilter {} + extends AbstractFilter + implements CommandFilter {} diff --git a/packages/framework/src/features/command/filter/middleware/CommandFilterCheckMiddleware.ts b/packages/framework/src/features/command/filter/middleware/CommandFilterCheckMiddleware.ts index 15e36e6e..65362fbd 100644 --- a/packages/framework/src/features/command/filter/middleware/CommandFilterCheckMiddleware.ts +++ b/packages/framework/src/features/command/filter/middleware/CommandFilterCheckMiddleware.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,10 +22,9 @@ * SOFTWARE. */ -import type { CommandData, ExecutableCommand } from '@nyx-discord/core'; +import type { AnyExecutableCommand } from '@nyx-discord/core'; import { BasicFilterCheckMiddleware } from '../../../../filter/middleware/BasicFilterCheckMiddleware.js'; -export class CommandFilterCheckMiddleware extends BasicFilterCheckMiddleware< - ExecutableCommand -> {} +// eslint-disable-next-line max-len +export class CommandFilterCheckMiddleware extends BasicFilterCheckMiddleware {} diff --git a/packages/framework/src/features/command/middleware/AbstractCommandMiddleware.ts b/packages/framework/src/features/command/middleware/AbstractCommandMiddleware.ts index 2bb7c75e..64ea09b4 100644 --- a/packages/framework/src/features/command/middleware/AbstractCommandMiddleware.ts +++ b/packages/framework/src/features/command/middleware/AbstractCommandMiddleware.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,14 +23,13 @@ */ import type { - CommandData, + AnyExecutableCommand, CommandExecutionArgs, - ExecutableCommand, + CommandMiddleware, } from '@nyx-discord/core'; import { AbstractMiddleware } from '../../../middleware/AbstractMiddleware.js'; -export abstract class AbstractCommandMiddleware extends AbstractMiddleware< - ExecutableCommand, - CommandExecutionArgs -> {} +export abstract class AbstractCommandMiddleware + extends AbstractMiddleware + implements CommandMiddleware {} diff --git a/packages/framework/src/features/command/repository/DefaultCommandRepository.ts b/packages/framework/src/features/command/repository/DefaultCommandRepository.ts index a4b634c8..f7f83591 100644 --- a/packages/framework/src/features/command/repository/DefaultCommandRepository.ts +++ b/packages/framework/src/features/command/repository/DefaultCommandRepository.ts @@ -25,74 +25,36 @@ import type { ReadonlyCollection } from '@discordjs/collection'; import { Collection } from '@discordjs/collection'; import type { - ArrayMinLength, ClassImplements, Command, - CommandData, - CommandReferenceData, CommandRepository, - ExecutableCommand, ImplementsParentCommand, ImplementsStandaloneCommand, ImplementsSubCommand, ImplementsSubCommandGroup, - ParentCommand, + SubCommand, + SubCommandGroup, TopLevelCommand, } from '@nyx-discord/core'; -import { - IllegalDuplicateError, - ObjectNotFoundError, - ResolvedCommandType, -} from '@nyx-discord/core'; - -import type { ApplicationCommand, Client } from 'discord.js'; - -import { CommandDataVisitor } from '../visitor/CommandDataVisitor.js'; +import { IllegalDuplicateError, ObjectNotFoundError } from '@nyx-discord/core'; export class DefaultCommandRepository implements CommandRepository { - protected readonly mappings: Collection< - string, - ArrayMinLength - > = new Collection>(); - protected readonly commands: Collection = new Collection(); - protected readonly client: Client; - - protected readonly refreshCommands: boolean; - - constructor(client: Client, refreshCommands: boolean) { - this.client = client; - this.refreshCommands = refreshCommands; - } - public get size() { return this.commands.size; } - public static create( - client: Client, - refreshCommands: boolean, - ): CommandRepository { - return new DefaultCommandRepository(client, refreshCommands); + public static create(): CommandRepository { + return new DefaultCommandRepository(); } public onSetup(): void { /** Do nothing by default. */ } - public async onStart(): Promise { - if (!this.refreshCommands) return; - - await this.refreshDiscord(); - } - - public onStop(): Promise | void { - /** Do nothing by default */ - } - - public async addCommand(command: TopLevelCommand): Promise { + public addCommand(command: TopLevelCommand): this { const commandId = command.getId(); const existentCommand = this.commands.get(commandId); if (existentCommand) { @@ -103,68 +65,23 @@ export class DefaultCommandRepository implements CommandRepository { ); } - /** - * If it's a standalone command, we need to check if another command with - * that already handles any of its contexts already exists. - */ - if (command.isStandaloneCommand()) { - const existentStandaloneCommand = this.commands.find( - (registeredCommand) => { - if ( - !registeredCommand.isStandaloneCommand() - || registeredCommand.getData().name !== command.getData().name - ) { - return false; - } - - const commandContexts = command.getContexts(); - const registeredCommandContexts = registeredCommand.getContexts(); - return commandContexts.some((element) => - registeredCommandContexts.includes(element), - ); - }, - ); - - if (existentStandaloneCommand) { - throw new IllegalDuplicateError( - existentStandaloneCommand, - command, - `The command '${command.constructor.name}' wasn't added. Another standalone command '${existentStandaloneCommand.constructor.name}' of the same name already exists that supports its same contexts.`, - ); - } - } - this.commands.set(commandId, command); - - await this.registerCommand(command); return this; } - public async removeCommand( - command: TopLevelCommand, - throwIfAbsent = true, - ): Promise { + public removeCommand(command: TopLevelCommand): this { const commandId = command.getId(); if (!this.commands.has(commandId)) { - if (!throwIfAbsent) return this; throw new ObjectNotFoundError( `The command '${command.constructor.name}' wasn't removed. A command with that ID doesn't exist.`, ); } this.commands.delete(commandId); - await this.refreshDiscord(); return this; } - public async updateParentCommand(command: ParentCommand): Promise { - await this.unregisterCommand(command); - await this.registerCommand(command); - - return this; - } - - public getCommandById(id: string): Command | null { + public getCommandByName(id: string): TopLevelCommand | null { return this.commands.get(id) ?? null; } @@ -172,35 +89,35 @@ export class DefaultCommandRepository implements CommandRepository { return this.commands.has(id); } - public isCommandInstance(instance: Command): boolean { + public isCommandInstance(instance: TopLevelCommand): boolean { return this.commands.find((command) => command === instance) !== undefined; } - public locateByTree( + public locateByClassTree( ParentCommandClass: ImplementsParentCommand, SubCommandGroupClass: T, ): InstanceType | null; - public locateByTree( + public locateByClassTree( ParentCommandClass: ImplementsParentCommand, SubCommandGroupClass: ImplementsSubCommandGroup, SubCommandClass: T, ): InstanceType | null; - public locateByTree( + public locateByClassTree( ParentCommandClass: ImplementsParentCommand, SubCommandClass: T, ): InstanceType | null; - public locateByTree( + public locateByClassTree( ParentCommandClass: T, ): InstanceType | null; - public locateByTree( + public locateByClassTree( StandaloneCommandClass: T, ): InstanceType | null; - public locateByTree>>( + public locateByClassTree>>( TopLevelCommandClass: ClassImplements, FirstChildClass?: ImplementsSubCommandGroup | ImplementsSubCommand, SecondChildClass?: ImplementsSubCommand, ): InstanceType | null { - let command: Command | null = + let command: Command | null = this.commands.find( (registeredCommand) => registeredCommand instanceof TopLevelCommandClass, @@ -209,7 +126,7 @@ export class DefaultCommandRepository implements CommandRepository { if (!FirstChildClass) return command as InstanceType; - if (command.isParentCommand()) { + if (command.isParent()) { command = command.findChildByClass(FirstChildClass); if (!command) return null; @@ -222,84 +139,48 @@ export class DefaultCommandRepository implements CommandRepository { return (command as InstanceType) ?? null; } - public locateByData( - data: CommandReferenceData, - ): ExecutableCommand | null { - const commands = this.commands.filter( - (command) => command.getData().name === data.root, - ); - if (!commands.size) return null; - - let topLevel; - if (data.type !== ResolvedCommandType.StandaloneCommand) { - topLevel = commands.first(); - } else { - topLevel = commands.find((command) => { - if (!command.isStandaloneCommand()) return false; - - const contexts = command.getContexts(); - const type = data.commandType; - - return contexts.includes(type); - }); + public locateByNameTree(parent: string): TopLevelCommand | null; + public locateByNameTree( + parent: string, + firstChild: string, + ): SubCommand | SubCommandGroup | null; + public locateByNameTree( + parent: string, + firstChild: string, + secondChild: string, + ): SubCommand | null; + public locateByNameTree( + parent: string, + firstChild?: string, + secondChild?: string, + ): TopLevelCommand | SubCommand | SubCommandGroup | null { + const parentCommand = this.commands.get(parent) ?? null; + if (!firstChild) return parentCommand; + if (!parentCommand || !parentCommand.isParent()) return null; + + const firstChildCommand = parentCommand.findChildByName(firstChild); + if (!secondChild) return firstChildCommand; + if (!firstChildCommand || !firstChildCommand.isSubCommandGroup()) { + return null; } - if (!topLevel) return null; - - if (topLevel.isStandaloneCommand()) { - return data.type === ResolvedCommandType.StandaloneCommand - ? topLevel - : null; - } - - if (data.type === ResolvedCommandType.StandaloneCommand) return null; - - const childName = - data.type === ResolvedCommandType.SubCommand - ? data.subCommand - : data.group; - - const child = topLevel.findChildByName(childName); - if (!child) return null; - - if (child.isSubCommand()) { - return data.type === ResolvedCommandType.SubCommand ? child : null; - } - - const subCommand = child.findChildByName(data.subCommand); - if (!subCommand) return null; - return data.type === ResolvedCommandType.SubCommandOnGroup - ? subCommand - : null; + return firstChildCommand.findChildByName(secondChild); } public getCommands(): ReadonlyCollection { return this.commands; } - public getMappings(): ReadonlyCollection< - string, - ArrayMinLength - > { - return this.mappings; - } - public *entries(): IterableIterator<[string, TopLevelCommand]> { - for (const [key, item] of this.commands.entries()) { - yield [key, item]; - } + yield* this.commands.entries(); } public *keys(): IterableIterator { - for (const key of this.commands.keys()) { - yield key; - } + yield* this.commands.keys(); } public *values(): IterableIterator { - for (const item of this.commands.values()) { - yield item; - } + yield* this.commands.values(); } public next(): IteratorResult<[string, TopLevelCommand]> { @@ -309,79 +190,4 @@ export class DefaultCommandRepository implements CommandRepository { public [Symbol.iterator](): IterableIterator<[string, TopLevelCommand]> { return this.entries(); } - - /** Utility function to register a single command to Discord. */ - protected async registerCommand(command: TopLevelCommand): Promise { - const applicationCommandManager = this.client.application?.commands; - if (!applicationCommandManager) return; - - const visitor = new CommandDataVisitor(command); - const datas = visitor.visit(); - - const addPromises: Promise[] = datas.map((data) => - applicationCommandManager.create(data), - ); - const applicationCommands = (await Promise.all( - addPromises.values(), - )) as ArrayMinLength; - - this.mappings.set(command.getId(), applicationCommands); - } - - protected async unregisterCommand(command: TopLevelCommand): Promise { - const applicationCommandManager = this.client.application?.commands; - if (!applicationCommandManager) return; - - const commandId = command.getId(); - const mappings = this.mappings.get(commandId); - - if (!mappings) return; - - const deletePromises: Promise[] = []; - - for (const mapping of mappings) { - deletePromises.push(mapping.delete()); - } - - await Promise.all(deletePromises); - this.mappings.delete(commandId); - } - - protected async refreshDiscord(): Promise { - const clientApplication = this.client.application; - - if (!clientApplication) return; - - await clientApplication.commands.set([]); - - // If no commands have been registered, simply set no commands. - if (!this.commands.size) { - return; - } - - const addPromises: Promise[] = []; - - for (const command of this.commands.values()) { - addPromises.push(this.registerCommand(command)); - } - - await Promise.all(addPromises); - } - - protected searchCommandForApplication( - application: ApplicationCommand, - ): TopLevelCommand | null { - const { name, type } = application; - const command = this.commands.find( - (command) => command.getData().name === name, - ); - - if (!command) return null; - if (!command.isStandaloneCommand()) return command; - - const contexts = command.getContexts(); - if (contexts.includes(type)) return command; - - return null; - } } diff --git a/packages/framework/src/features/command/resolver/DefaultCommandResolver.ts b/packages/framework/src/features/command/resolve/DefaultCommandResolver.ts similarity index 58% rename from packages/framework/src/features/command/resolver/DefaultCommandResolver.ts rename to packages/framework/src/features/command/resolve/DefaultCommandResolver.ts index 2c1578fc..29044488 100644 --- a/packages/framework/src/features/command/resolver/DefaultCommandResolver.ts +++ b/packages/framework/src/features/command/resolve/DefaultCommandResolver.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,13 +23,12 @@ */ import type { + AnyExecutableCommand, ApplicationCommandInteraction, - CommandReferenceData, + CommandRepository, CommandResolver, } from '@nyx-discord/core'; -import { ResolvedCommandType } from '@nyx-discord/core'; import type { AutocompleteInteraction } from 'discord.js'; -import { ApplicationCommandType } from 'discord.js'; export class DefaultCommandResolver implements CommandResolver { public static create(): CommandResolver { @@ -38,50 +37,47 @@ export class DefaultCommandResolver implements CommandResolver { public resolveFromCommandInteraction( interaction: ApplicationCommandInteraction | AutocompleteInteraction, - ): CommandReferenceData | null { + repository: CommandRepository, + ): AnyExecutableCommand | null { const { commandName } = interaction; - if (interaction.commandType !== ApplicationCommandType.ChatInput) { - return { - type: ResolvedCommandType.StandaloneCommand, - root: commandName, - commandType: interaction.commandType, - }; + if (!interaction.isChatInputCommand()) { + return this.findTopLevelExecutable(commandName, repository); } - const subCommandName = interaction.options.getSubcommand(false); - const groupName = interaction.options.getSubcommandGroup(false); + const group = interaction.options.getSubcommandGroup(false); + const subcommand = interaction.options.getSubcommand(false); - if (!subCommandName && !groupName) { - return { - type: ResolvedCommandType.StandaloneCommand, - root: commandName, - commandType: interaction.commandType, - }; + if (!group && !subcommand) { + return this.findTopLevelExecutable(commandName, repository); } - if (!groupName && subCommandName) { - return { - type: ResolvedCommandType.SubCommand, - root: commandName, - subCommand: subCommandName, - }; + if (group) { + if (!subcommand) return null; + return repository.locateByNameTree(commandName, group, subcommand); } - if (groupName && subCommandName) { - return { - type: ResolvedCommandType.SubCommandOnGroup, - root: commandName, - subCommand: subCommandName, - group: groupName, - }; + if (subcommand) { + const found = repository.locateByNameTree(commandName, subcommand); + if (!found || found.isSubCommandGroup()) return null; + return found; } - return null; + return this.findTopLevelExecutable(commandName, repository); } public resolveFromAutocompleteInteraction( interaction: AutocompleteInteraction, + repository: CommandRepository, ) { - return this.resolveFromCommandInteraction(interaction); + return this.resolveFromCommandInteraction(interaction, repository); + } + + protected findTopLevelExecutable( + commandName: string, + repository: CommandRepository, + ): AnyExecutableCommand | null { + const found = repository.locateByNameTree(commandName); + if (!found || found.isParent()) return null; + return found; } } diff --git a/packages/framework/src/features/command/visitor/CommandDataVisitor.ts b/packages/framework/src/features/command/visitor/CommandDataVisitor.ts deleted file mode 100644 index 57963aa2..00000000 --- a/packages/framework/src/features/command/visitor/CommandDataVisitor.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Amgelo563 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import type { - CommandOptionData, - CommandVisitor, - ParentCommand, - StandaloneCommand, - SubCommand, - SubCommandGroup, - TopLevelCommand, -} from '@nyx-discord/core'; -import type { - ApplicationCommandData, - ApplicationCommandOptionData, - ApplicationCommandSubCommandData, - ApplicationCommandSubGroupData, - ChatInputApplicationCommandData, - MessageApplicationCommandData, - UserApplicationCommandData, -} from 'discord.js'; -import { - ApplicationCommandOptionType, - ApplicationCommandType, -} from 'discord.js'; - -/** An object for visiting a command structure and extracting its data. */ -export class CommandDataVisitor - implements CommandVisitor -{ - /** The currently visited command. */ - protected command: TopLevelCommand; - - /** The generated data so far. */ - protected data: { - ChatInput?: ChatInputApplicationCommandData & { - options?: ApplicationCommandOptionData[]; - }; - User?: UserApplicationCommandData; - Message?: MessageApplicationCommandData; - } = {}; - - constructor(command: TopLevelCommand) { - this.command = command; - } - - /** Visit the current command object. */ - public visit(): ApplicationCommandData[] { - this.command.isParentCommand() - ? this.visitParentCommand(this.command) - : this.visitStandaloneCommand(this.command); - return Array.from(Object.values(this.data)); - } - - public visitStandaloneCommand(command: StandaloneCommand): void { - const data = this.command.getData(); - const contextMenus = command.getContexts(); - if (contextMenus.includes(ApplicationCommandType.User)) { - this.data.User = { name: data.name, type: ApplicationCommandType.User }; - } - if (contextMenus.includes(ApplicationCommandType.Message)) { - this.data.Message = { - name: data.name, - type: ApplicationCommandType.Message, - }; - } - if (contextMenus.includes(ApplicationCommandType.ChatInput)) { - this.data.ChatInput = { - ...data, - type: ApplicationCommandType.ChatInput, - options: command.getOptions() as ApplicationCommandOptionData[], - }; - } - } - - public visitParentCommand(command: ParentCommand): void { - if (command.size === 0 || command.size > 25) { - throw new RangeError( - `Can't create a parent command with children size of ${command.size}.`, - ); - } - - for (const child of command.getChildren()) child.acceptVisitor(this); - } - - public visitSubCommand(subcommand: SubCommand): void { - let { ChatInput } = this.data; - ChatInput ??= { - ...this.command.getData(), - type: ApplicationCommandType.ChatInput, - }; - - ChatInput.options ??= []; - ChatInput.options.push({ - ...subcommand.getData(), - type: ApplicationCommandOptionType.Subcommand, - options: subcommand.getOptions() as CommandOptionData[], - }); - - this.data.ChatInput = ChatInput; - } - - public visitSubCommandGroup(group: SubCommandGroup): void { - if (group.size === 0 || group.size > 25) { - throw new RangeError( - `Can't create a subcommand group with children size of ${group.size}.`, - ); - } - - const options: ApplicationCommandSubCommandData[] = []; - for (const subcommand of group.getChildren()) { - options.push({ - ...subcommand.getData(), - type: ApplicationCommandOptionType.Subcommand, - options: subcommand.getOptions() as CommandOptionData[], - }); - } - - const data: ApplicationCommandSubGroupData = { - ...group.getData(), - type: ApplicationCommandOptionType.SubcommandGroup, - options, - }; - - this.data.ChatInput ??= { - ...this.command.getData(), - type: ApplicationCommandType.ChatInput, - }; - this.data.ChatInput.options ??= []; - - const { ChatInput } = this.data; - ChatInput.options ??= []; - ChatInput.options.push(data); - } - - public refresh(command: TopLevelCommand) { - this.command = command; - this.data = {}; - } -} diff --git a/packages/framework/src/features/session/customId/DefaultSessionCustomIdCodec.ts b/packages/framework/src/features/session/customId/DefaultSessionCustomIdCodec.ts index 521baf05..b4ae1b5f 100644 --- a/packages/framework/src/features/session/customId/DefaultSessionCustomIdCodec.ts +++ b/packages/framework/src/features/session/customId/DefaultSessionCustomIdCodec.ts @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Amgelo563 + * Copyright (c) 2024 Amgelo563 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,11 +28,10 @@ import type { SessionCustomIdCodec, } from '@nyx-discord/core'; import { PaginationCustomIdBuilder } from '@nyx-discord/core'; - -import { BasicCustomIdCodec } from '../../../customId/BasicCustomIdCodec.js'; +import { IdentifiableCustomIdCodec } from '../../../customId/IdentifiableCustomIdCodec'; export class DefaultSessionCustomIdCodec - extends BasicCustomIdCodec> + extends IdentifiableCustomIdCodec> implements SessionCustomIdCodec { public static readonly DefaultNamespace = 'SSN'; diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index 632ea130..6c14bcaf 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -3,31 +3,33 @@ */ export * from './bot/Bot'; -export * from './customId/BasicCustomIdCodec'; +export * from './customId/AbstractCustomIdCodec'; +export * from './customId/IdentifiableCustomIdCodec'; export * from './discord/ActionRowList'; export * from './discord/ActionRowWrapper'; export * from './error/BasicErrorHandler'; export * from './errors/NotImplementedError'; export * from './features/command/DefaultCommandManager'; +export * from './features/command/commands/AbstractCommand'; +export * from './features/command/commands/AbstractContextMenuCommand'; export * from './features/command/commands/AbstractParentCommand'; export * from './features/command/commands/AbstractStandaloneCommand'; export * from './features/command/commands/AbstractSubCommand'; export * from './features/command/commands/AbstractSubCommandGroup'; -export * from './features/command/commands/abstract/AbstractChildableCommand'; -export * from './features/command/commands/abstract/AbstractCommand'; -export * from './features/command/commands/abstract/AbstractExecutableCommand'; +export * from './features/command/commands/child/AbstractChildableCommand'; +export * from './features/command/commands/executable/AbstractExecutableCommand'; export * from './features/command/customId/DefaultCommandCustomIdCodec'; +export * from './features/command/deploy/DefaultCommandDeployer'; export * from './features/command/events/DefaultCommandAutocompleteSubscriber'; export * from './features/command/events/DefaultCommandInteractionSubscriber'; export * from './features/command/events/DefaultCommandSubscriptionsContainer'; -export * from './features/command/executor/DefaultCommandExecutor'; +export * from './features/command/execution/DefaultCommandExecutor'; export * from './features/command/filter/AbstractCommandFilter'; export * from './features/command/filter/middleware/CommandFilterCheckMiddleware'; export * from './features/command/middleware/AbstractCommandMiddleware'; export * from './features/command/middleware/CommandMiddlewareList'; export * from './features/command/repository/DefaultCommandRepository'; -export * from './features/command/resolver/DefaultCommandResolver'; -export * from './features/command/visitor/CommandDataVisitor'; +export * from './features/command/resolve/DefaultCommandResolver'; export * from './features/event/DefaultEventManager'; export * from './features/event/bus/BasicEventBus'; export * from './features/event/bus/BasicEventEmitterBus';