refactor: initial moves
This commit is contained in:
22
bot/lib/handlers/AutocompleteHandler.ts
Normal file
22
bot/lib/handlers/AutocompleteHandler.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AutocompleteInteraction } from "discord.js";
|
||||
import { AuroraClient } from "@/lib/BotClient";
|
||||
|
||||
|
||||
/**
|
||||
* Handles autocomplete interactions for slash commands
|
||||
*/
|
||||
export class AutocompleteHandler {
|
||||
static async handle(interaction: AutocompleteInteraction): Promise<void> {
|
||||
const command = AuroraClient.commands.get(interaction.commandName);
|
||||
|
||||
if (!command || !command.autocomplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.autocomplete(interaction);
|
||||
} catch (error) {
|
||||
console.error(`Error handling autocomplete for ${interaction.commandName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
bot/lib/handlers/CommandHandler.test.ts
Normal file
59
bot/lib/handlers/CommandHandler.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, test, expect, mock, beforeEach } from "bun:test";
|
||||
import { CommandHandler } from "./CommandHandler";
|
||||
import { AuroraClient } from "@/lib/BotClient";
|
||||
import { ChatInputCommandInteraction } from "discord.js";
|
||||
|
||||
// Mock UserService
|
||||
mock.module("@/modules/user/user.service", () => ({
|
||||
userService: {
|
||||
getOrCreateUser: mock(() => Promise.resolve())
|
||||
}
|
||||
}));
|
||||
|
||||
describe("CommandHandler", () => {
|
||||
beforeEach(() => {
|
||||
AuroraClient.commands.clear();
|
||||
AuroraClient.lastCommandTimestamp = null;
|
||||
});
|
||||
|
||||
test("should update lastCommandTimestamp on successful execution", async () => {
|
||||
const executeSuccess = mock(() => Promise.resolve());
|
||||
AuroraClient.commands.set("test", {
|
||||
data: { name: "test" } as any,
|
||||
execute: executeSuccess
|
||||
} as any);
|
||||
|
||||
const interaction = {
|
||||
commandName: "test",
|
||||
user: { id: "123", username: "testuser" }
|
||||
} as unknown as ChatInputCommandInteraction;
|
||||
|
||||
await CommandHandler.handle(interaction);
|
||||
|
||||
expect(executeSuccess).toHaveBeenCalled();
|
||||
expect(AuroraClient.lastCommandTimestamp).not.toBeNull();
|
||||
expect(AuroraClient.lastCommandTimestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should not update lastCommandTimestamp on failed execution", async () => {
|
||||
const executeError = mock(() => Promise.reject(new Error("Command Failed")));
|
||||
AuroraClient.commands.set("fail", {
|
||||
data: { name: "fail" } as any,
|
||||
execute: executeError
|
||||
} as any);
|
||||
|
||||
const interaction = {
|
||||
commandName: "fail",
|
||||
user: { id: "123", username: "testuser" },
|
||||
replied: false,
|
||||
deferred: false,
|
||||
reply: mock(() => Promise.resolve()),
|
||||
followUp: mock(() => Promise.resolve())
|
||||
} as unknown as ChatInputCommandInteraction;
|
||||
|
||||
await CommandHandler.handle(interaction);
|
||||
|
||||
expect(executeError).toHaveBeenCalled();
|
||||
expect(AuroraClient.lastCommandTimestamp).toBeNull();
|
||||
});
|
||||
});
|
||||
41
bot/lib/handlers/CommandHandler.ts
Normal file
41
bot/lib/handlers/CommandHandler.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
|
||||
import { AuroraClient } from "@/lib/BotClient";
|
||||
import { userService } from "@shared/modules/user/user.service";
|
||||
import { createErrorEmbed } from "@lib/embeds";
|
||||
|
||||
|
||||
/**
|
||||
* Handles slash command execution
|
||||
* Includes user validation and comprehensive error handling
|
||||
*/
|
||||
export class CommandHandler {
|
||||
static async handle(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const command = AuroraClient.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure user exists in database
|
||||
try {
|
||||
await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
||||
} catch (error) {
|
||||
console.error("Failed to ensure user exists:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
AuroraClient.lastCommandTimestamp = Date.now();
|
||||
} catch (error) {
|
||||
console.error(String(error));
|
||||
const errorEmbed = createErrorEmbed('There was an error while executing this command!');
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
|
||||
} else {
|
||||
await interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
bot/lib/handlers/ComponentInteractionHandler.ts
Normal file
78
bot/lib/handlers/ComponentInteractionHandler.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ButtonInteraction, StringSelectMenuInteraction, ModalSubmitInteraction, MessageFlags } from "discord.js";
|
||||
|
||||
import { UserError } from "@lib/errors";
|
||||
import { createErrorEmbed } from "@lib/embeds";
|
||||
|
||||
type ComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction;
|
||||
|
||||
/**
|
||||
* Handles component interactions (buttons, select menus, modals)
|
||||
* Routes to appropriate handlers based on customId patterns
|
||||
* Provides centralized error handling with UserError differentiation
|
||||
*/
|
||||
export class ComponentInteractionHandler {
|
||||
static async handle(interaction: ComponentInteraction): Promise<void> {
|
||||
const { interactionRoutes } = await import("@lib/interaction.routes");
|
||||
|
||||
for (const route of interactionRoutes) {
|
||||
if (route.predicate(interaction)) {
|
||||
const module = await route.handler();
|
||||
const handlerMethod = module[route.method];
|
||||
|
||||
if (typeof handlerMethod === 'function') {
|
||||
try {
|
||||
await handlerMethod(interaction);
|
||||
return;
|
||||
} catch (error) {
|
||||
await this.handleError(interaction, error, route.method);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.error(`Handler method ${route.method} not found in module`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors from interaction handlers
|
||||
* Differentiates between UserError (user-facing) and system errors
|
||||
*/
|
||||
private static async handleError(
|
||||
interaction: ComponentInteraction,
|
||||
error: unknown,
|
||||
handlerName: string
|
||||
): Promise<void> {
|
||||
const isUserError = error instanceof UserError;
|
||||
|
||||
// Determine error message
|
||||
const errorMessage = isUserError
|
||||
? (error as Error).message
|
||||
: 'An unexpected error occurred. Please try again later.';
|
||||
|
||||
// Log system errors (non-user errors) for debugging
|
||||
if (!isUserError) {
|
||||
console.error(`Error in ${handlerName}:`, error);
|
||||
}
|
||||
|
||||
const errorEmbed = createErrorEmbed(errorMessage);
|
||||
|
||||
try {
|
||||
// Handle different interaction states
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({
|
||||
embeds: [errorEmbed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
} catch (replyError) {
|
||||
// If we can't send a reply, log it
|
||||
console.error(`Failed to send error response in ${handlerName}:`, replyError);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
bot/lib/handlers/index.ts
Normal file
3
bot/lib/handlers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ComponentInteractionHandler } from "./ComponentInteractionHandler";
|
||||
export { AutocompleteHandler } from "./AutocompleteHandler";
|
||||
export { CommandHandler } from "./CommandHandler";
|
||||
Reference in New Issue
Block a user