Skip to content

Commit

Permalink
refactor: use coral-command
Browse files Browse the repository at this point in the history
  • Loading branch information
didinele committed Jul 9, 2024
1 parent 4673153 commit f2655d9
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 67 deletions.
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@
"node": ">=16.9.0"
},
"devDependencies": {
"@types/node": "^20.14.6",
"@types/node": "^20.14.10",
"@types/pg": "^8.11.6",
"typescript": "^5.4.5"
},
"dependencies": {
"@chatsift/pino-rotate-file": "^0.3.0",
"@discordjs/brokers": "^1.0.0-dev.1720311088-d8e94d8f1",
"@discordjs/builders": "^1.8.2",
"@discordjs/core": "^1.2.0",
"@discordjs/rest": "^2.3.0",
"@msgpack/msgpack": "^3.0.0-beta2",
"@naval-base/ms": "^3.1.0",
"@sapphire/bitfield": "^1.2.2",
"@sapphire/discord-utilities": "^3.3.0",
"coral-command": "^0.4.1",
"inversify": "^6.0.2",
"ioredis": "5.3.2",
"kysely": "^0.27.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import {
type RESTPostAPIApplicationCommandsJSONBody,
} from '@discordjs/core';
import { InteractionOptionResolver } from '@sapphire/discord-utilities';
import {
type InteractionHandler as CoralInteractionHandler,
Actions,
Actions as CoralActions,
Executor as CoralExecutor,
ExecutorEvents,
} from 'coral-command';
import { inject, injectable } from 'inversify';
import type { Selectable } from 'kysely';
import type { Logger } from 'pino';
import { type Logger } from 'pino';
import type { IDataManager } from '../applicationData/IDataManager.js';
import { INJECTION_TOKENS } from '../container.js';
import type { Incident } from '../db.js';
Expand All @@ -27,23 +34,41 @@ import {
} from './ICommandHandler.js';

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

readonly #handlers = {
applicationCommands: new Map<ApplicationCommandIdentifier, ApplicationCommandHandler>(),
components: new Map<string, ComponentHandler>(),
autocomplete: new Map<AutocompleteIdentifier, AutocompleteHandler>(),
modals: new Map<string, ModalHandler>(),
applicationCommands: new Map<ApplicationCommandIdentifier, ApplicationCommandHandler<CoralInteractionHandler>>(),
components: new Map<string, ComponentHandler<CoralInteractionHandler>>(),
autocomplete: new Map<AutocompleteIdentifier, AutocompleteHandler<CoralInteractionHandler>>(),
modals: new Map<string, ModalHandler<CoralInteractionHandler>>(),
} as const;

readonly #executor: CoralExecutor;

public constructor(
private readonly api: API,
private readonly database: IDataManager,
private readonly env: Env,
@inject(INJECTION_TOKENS.logger) private readonly logger: Logger,
) {
super();

this.#executor = new CoralExecutor(api, env.discordClientId)
.on(ExecutorEvents.CallbackError, (error) => this.logger.error(error, 'Unhandled error in command executor'))
.on(ExecutorEvents.HandlerError, async (error, actions) => {
this.logger.error(error, 'Unhandled error in command handler');

const incident =
error instanceof Error
? await this.database.createIncident(error, actions.interaction.guild_id)
: await this.database.createIncident(
new Error("Handler threw non-error. We don't have a stack."),
actions.interaction.guild_id,
);

return this.reportIncident(actions, incident);
});
}

public async deployCommands(): Promise<void> {
Expand Down Expand Up @@ -77,7 +102,7 @@ export class CommandHandler extends ICommandHandler {
new Error('Command handler not found'),
interaction.guild_id,
);
await this.reportIncident(interaction, incident);
await this.reportIncident(this.interactionToActions(interaction), incident);

break;
}
Expand All @@ -94,7 +119,7 @@ export class CommandHandler extends ICommandHandler {
new Error('Component handler not found'),
interaction.guild_id,
);
await this.reportIncident(interaction, incident);
await this.reportIncident(this.interactionToActions(interaction), incident);

break;
}
Expand All @@ -121,7 +146,7 @@ export class CommandHandler extends ICommandHandler {
new Error('Autocomplete handler not found'),
interaction.guild_id,
);
await this.reportIncident(interaction, incident);
await this.reportIncident(this.interactionToActions(interaction), incident);

break;
}
Expand All @@ -135,7 +160,7 @@ export class CommandHandler extends ICommandHandler {
}

const incident = await this.database.createIncident(new Error('Modal handler not found'), interaction.guild_id);
await this.reportIncident(interaction, incident);
await this.reportIncident(this.interactionToActions(interaction), incident);

break;
}
Expand Down Expand Up @@ -197,34 +222,25 @@ export class CommandHandler extends ICommandHandler {
return { root: identifier };
}

// TODO: Generalize
private async reportIncident(interaction: APIInteraction, incident: Selectable<Incident>): Promise<void> {
await this.api.interactions.reply(interaction.id, interaction.token, {
// TODO: Handle specific errors maybe

Check warning on line 225 in packages/core/src/command-framework/CoralCommandHandler.ts

View workflow job for this annotation

GitHub Actions / Quality Check

Unexpected 'todo' comment: 'TODO: Handle specific errors maybe'
private async reportIncident(actions: CoralActions, incident: Selectable<Incident>): Promise<void> {
await actions.respond({
content: `An error occurred while processing your request. Please report this incident to the developers. (Incident ID: ${incident.id})`,
});
}

private interactionToActions(interaction: APIInteraction): Actions {
return new Actions(this.api, this.env.discordClientId, interaction);
}

private wrapHandler<HandlerArgs extends [APIInteraction, ...any[]]>(
handler: (...args: HandlerArgs) => Promise<void>,
handler: (...args: HandlerArgs) => CoralInteractionHandler,
): (...args: HandlerArgs) => Promise<void> {
return async (...args) => {
const [interaction] = args;

try {
await handler(...args);
} catch (error) {
this.logger.error(error, 'Unhandled error in command handler');

const incident =
error instanceof Error
? await this.database.createIncident(error, interaction.guild_id)
: await this.database.createIncident(
new Error("Handler threw non-error. We don't have a stack."),
interaction.guild_id,
);

return this.reportIncident(interaction, incident);
}
const generator = handler(...args);
await this.#executor.handleInteraction(generator, interaction);
};
}
}
35 changes: 19 additions & 16 deletions packages/core/src/command-framework/ICommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,53 +27,56 @@ export type ApplicationCommandIdentifier = `${string}:${string}:${string}`;
/**
* Callback responsible for handling application commands.
*/
export type ApplicationCommandHandler = (
export type ApplicationCommandHandler<TReturnType = any> = (
interaction: APIApplicationCommandInteraction,
options: InteractionOptionResolver,
) => Promise<void>;
) => TReturnType;

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

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

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

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

export interface HandlerModule {
register(handler: ICommandHandler): void;
export interface HandlerModule<TReturnType> {
register(handler: ICommandHandler<TReturnType>): void;
}

export type HandlerModuleConstructor = new (...args: unknown[]) => HandlerModule;
export type HandlerModuleConstructor<TReturnType> = new (...args: unknown[]) => HandlerModule<TReturnType>;

export const BASE_HANDLERS_PATH = join(dirname(fileURLToPath(import.meta.url)), 'handlers');

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

export abstract class ICommandHandler {
export abstract class ICommandHandler<TReturnType> {
public abstract handle(interaction: APIInteraction): Promise<void>;
public abstract register(options: RegisterOptions): void;
public abstract register(options: RegisterOptions<TReturnType>): void;
public abstract deployCommands(): Promise<void>;
}
35 changes: 20 additions & 15 deletions packages/core/src/command-framework/handlers/dev.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { API, InteractionContextType, type APIInteraction } from '@discordjs/core';
import { InteractionContextType, type APIInteraction } from '@discordjs/core';
import { ActionKind, type InteractionHandler as CoralInteractionHandler } from 'coral-command';
import { injectable } from 'inversify';
import type { Env } from '../../util/Env.js';
import type { ApplicationCommandHandler, HandlerModule, ICommandHandler } from '../ICommandHandler.js';
import type { HandlerModule, ICommandHandler } from '../ICommandHandler.js';

@injectable()
export default class DevHandler implements HandlerModule {
export default class DevHandler implements HandlerModule<CoralInteractionHandler> {
public constructor(
private readonly api: API,
private readonly handler: ICommandHandler,
private readonly handler: ICommandHandler<CoralInteractionHandler>,
private readonly env: Env,
) {}

Expand All @@ -21,25 +21,30 @@ export default class DevHandler implements HandlerModule {
contexts: [InteractionContextType.BotDM],
},
],
applicationCommands: [['deploy:none:none', this.handleDeploy]],
applicationCommands: [['deploy:none:none', this.handleDeploy.bind(this)]],
});
}

private readonly handleDeploy: ApplicationCommandHandler = async (interaction: APIInteraction) => {
public async *handleDeploy(interaction: APIInteraction): CoralInteractionHandler {
if (!interaction.user) {
throw new Error('Command /deploy was ran in non-dm.');
}

if (!this.env.admins.has(interaction.user.id)) {
await this.api.interactions.reply(interaction.id, interaction.token, {
content: 'You are not authorized to use this command',
});

return;
return {
action: ActionKind.Respond,
data: {
content: 'You are not authorized to use this command',
},
};
}

await this.handler.deployCommands();

await this.api.interactions.reply(interaction.id, interaction.token, { content: 'Successfully deployed commands' });
};
yield {
action: ActionKind.Respond,
data: {
content: 'Successfully deployed commands',
},
};
}
}
5 changes: 5 additions & 0 deletions packages/core/src/util/DependencyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { Logger } from 'pino';
import createPinoLogger from 'pino';
import { GuildCacheEntity, type CachedGuild } from '../cache/entities/GuildCacheEntity.js';
import type { ICacheEntity } from '../cache/entities/ICacheEntity.js';
import { CoralCommandHandler } from '../command-framework/CoralCommandHandler.js';
import { ICommandHandler } from '../command-framework/ICommandHandler.js';
import { INJECTION_TOKENS, globalContainer } from '../container.js';
import type { DB } from '../db.js';
import { Env } from './Env.js';
Expand Down Expand Up @@ -91,5 +93,8 @@ export class DependencyManager {
.bind<ICacheEntity<CachedGuild>>(INJECTION_TOKENS.cacheEntities.guild)
.to(GuildCacheEntity)
.inSingletonScope();

// command handler
globalContainer.bind<ICommandHandler<any>>(ICommandHandler).to(CoralCommandHandler).inSingletonScope();
}
}
2 changes: 1 addition & 1 deletion services/discord-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"node": ">=16.9.0"
},
"devDependencies": {
"@types/node": "^20.14.6",
"@types/node": "^20.14.10",
"pino": "^9.2.0",
"typescript": "^5.4.5"
},
Expand Down
2 changes: 1 addition & 1 deletion services/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"node": ">=16.9.0"
},
"devDependencies": {
"@types/node": "^20.14.6",
"@types/node": "^20.14.10",
"pino": "^9.2.0",
"typescript": "^5.4.5"
},
Expand Down
Loading

0 comments on commit f2655d9

Please sign in to comment.