Skip to content

Commit

Permalink
feat: command handler registration
Browse files Browse the repository at this point in the history
  • Loading branch information
didinele committed Jun 27, 2024
1 parent 09b1a13 commit 906aa0d
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 56 deletions.
102 changes: 50 additions & 52 deletions packages/core/src/command-framework/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import {
InteractionType,
type APIApplicationCommandAutocompleteInteraction,
type APIApplicationCommandInteraction,
type APIApplicationCommandInteractionDataIntegerOption,
type APIApplicationCommandInteractionDataNumberOption,
type APIApplicationCommandInteractionDataStringOption,
type APIInteraction,
type APIMessageComponentInteraction,
type APIModalSubmitInteraction,
type RESTPostAPIApplicationCommandsJSONBody,
} from '@discordjs/core';
import { InteractionOptionResolver } from '@sapphire/discord-utilities';
import { inject, injectable } from 'inversify';
Expand All @@ -17,56 +13,23 @@ import type { Logger } from 'pino';
import type { IDataManager } from '../applicationData/IDataManager.js';
import { INJECTION_TOKENS } from '../container.js';
import type { Incident } from '../db.js';
import { ICommandHandler } from './ICommandHandler.js';

/**
* @internal
*/
interface ResolvedCommandIdentifier {
root: ApplicationCommandIdentifier;
subcommand?: ApplicationCommandIdentifier;
}

/**
* The identifier of a command.
* `name:group:subcommand`
*/
export type ApplicationCommandIdentifier = `${string}:${string}:${string}`;

/**
* Callback responsible for handling application commands.
*/
export type ApplicationCommandHandler = (
interaction: APIApplicationCommandInteraction,
options: InteractionOptionResolver,
) => Promise<void>;

/**
* Callback responsible for handling components.
*/
export type ComponentHandler = (interaction: APIMessageComponentInteraction, args: string[]) => Promise<void>;

// [command]:argName
export type AutocompleteIdentifier = `${ApplicationCommandIdentifier}:${string}`;

/**
* Callback responsible for handling autocompletes.
*/
export type AutocompleteHandler = (
interaction: APIApplicationCommandAutocompleteInteraction,
option:
| APIApplicationCommandInteractionDataIntegerOption
| APIApplicationCommandInteractionDataNumberOption
| APIApplicationCommandInteractionDataStringOption,
) => Promise<void>;

/**
* Callback responsible for handling modals.
*/
export type ModalHandler = (interaction: APIModalSubmitInteraction, args: string[]) => Promise<void>;
import type { Env } from '../util/Env.js';
import {
ICommandHandler,
type ApplicationCommandHandler,
type ApplicationCommandIdentifier,
type AutocompleteHandler,
type AutocompleteIdentifier,
type ComponentHandler,
type ModalHandler,
type RegisterOptions,
type ResolvedCommandIdentifier,
} from './ICommandHandler.js';

@injectable()
export class CommandHandler extends ICommandHandler {
readonly #interactions: RESTPostAPIApplicationCommandsJSONBody[] = [];

readonly #handlers = {
applicationCommands: new Map<ApplicationCommandIdentifier, ApplicationCommandHandler>(),
components: new Map<string, ComponentHandler>(),
Expand All @@ -77,11 +40,16 @@ export class CommandHandler extends ICommandHandler {
public constructor(
private readonly api: API,
private readonly database: IDataManager,
private readonly env: Env,
@inject(INJECTION_TOKENS.logger) private readonly logger: Logger,
) {
super();
}

public async deployCommands(): Promise<void> {
await this.api.applicationCommands.bulkOverwriteGlobalCommands(this.env.discordClientId, this.#interactions);
}

public async handle(interaction: APIInteraction): Promise<void> {
if (!interaction.guild_id) {
const incident = await this.database.createIncident(new Error('Interaction not in guild'));
Expand Down Expand Up @@ -181,6 +149,36 @@ export class CommandHandler extends ICommandHandler {
}
}

public register(options: RegisterOptions): void {
if (options.interactions) {
this.#interactions.push(...options.interactions);
}

if (options.applicationCommands?.length) {
for (const [identifier, handler] of options.applicationCommands) {
this.#handlers.applicationCommands.set(identifier, handler);
}
}

if (options.components?.length) {
for (const [name, handler] of options.components) {
this.#handlers.components.set(name, handler);
}
}

if (options.autocomplete?.length) {
for (const [identifier, handler] of options.autocomplete) {
this.#handlers.autocomplete.set(identifier, handler);
}
}

if (options.modals?.length) {
for (const [name, handler] of options.modals) {
this.#handlers.modals.set(name, handler);
}
}
}

/**
* Resolves the command identifier from the interaction.
*
Expand Down
66 changes: 65 additions & 1 deletion packages/core/src/command-framework/ICommandHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
import type { APIInteraction } from '@discordjs/core';
import type {
APIApplicationCommandAutocompleteInteraction,
APIApplicationCommandInteraction,
APIApplicationCommandInteractionDataIntegerOption,
APIApplicationCommandInteractionDataNumberOption,
APIApplicationCommandInteractionDataStringOption,
APIInteraction,
APIMessageComponentInteraction,
APIModalSubmitInteraction,
RESTPostAPIApplicationCommandsJSONBody,
} from '@discordjs/core';
import type { InteractionOptionResolver } from '@sapphire/discord-utilities';

export interface ResolvedCommandIdentifier {
root: ApplicationCommandIdentifier;
subcommand?: ApplicationCommandIdentifier;
}

/**
* The identifier of a command.
* `name:group:subcommand`
*/
export type ApplicationCommandIdentifier = `${string}:${string}:${string}`;

/**
* Callback responsible for handling application commands.
*/
export type ApplicationCommandHandler = (
interaction: APIApplicationCommandInteraction,
options: InteractionOptionResolver,
) => Promise<void>;

/**
* Callback responsible for handling components.
*/
export type ComponentHandler = (interaction: APIMessageComponentInteraction, args: string[]) => Promise<void>;

// [command]:argName
export type AutocompleteIdentifier = `${ApplicationCommandIdentifier}:${string}`;

/**
* Callback responsible for handling autocompletes.
*/
export type AutocompleteHandler = (
interaction: APIApplicationCommandAutocompleteInteraction,
option:
| APIApplicationCommandInteractionDataIntegerOption
| APIApplicationCommandInteractionDataNumberOption
| APIApplicationCommandInteractionDataStringOption,
) => Promise<void>;

/**
* Callback responsible for handling modals.
*/
export type ModalHandler = (interaction: APIModalSubmitInteraction, args: string[]) => Promise<void>;

export interface RegisterOptions {
applicationCommands?: [ApplicationCommandIdentifier, ApplicationCommandHandler][];
autocomplete?: [AutocompleteIdentifier, AutocompleteHandler][];
components?: [string, ComponentHandler][];
interactions?: RESTPostAPIApplicationCommandsJSONBody[];
modals?: [string, ModalHandler][];
}

export abstract class ICommandHandler {
public abstract handle(interaction: APIInteraction): Promise<void>;
public abstract register(options: RegisterOptions): void;
public abstract deployCommands(): Promise<void>;
}
9 changes: 7 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ export * from './cache/entities/ICacheEntity.js';
export * from './cache/CacheFactory.js';
export * from './cache/ICache.js';

export * from './singletons/DependencyManager.js';
export * from './singletons/Env.js';
// Deliberately don't export impl
export * from './command-framework/ICommandHandler.js';

// Deliberately don't export impl
export * from './experiments/IExperimentHandler.js';

export * from './util/DependencyManager.js';
export * from './util/encode.js';
export * from './util/Env.js';
export * from './util/parseRelativeTime.js';
export * from './util/PermissionsBitField.js';
export * from './util/promiseAllObject.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { GuildCacheEntity, type CachedGuild } from '../cache/entities/GuildCache
import type { ICacheEntity } from '../cache/entities/ICacheEntity.js';
import { INJECTION_TOKENS, globalContainer } from '../container.js';
import type { DB } from '../db.js';
import type { TransportOptions } from '../util/loggingTransport.js';
import { Env } from './Env.js';
import type { TransportOptions } from './loggingTransport.js';

// no proper ESM support
const {
Expand Down
File renamed without changes.

0 comments on commit 906aa0d

Please sign in to comment.