feat: Implement dynamic event loading and refactor event handlers into dedicated files.

This commit is contained in:
syntaxbullet
2025-12-14 22:21:28 +01:00
parent 32c614975e
commit 1eace32aa1
8 changed files with 166 additions and 78 deletions

View File

@@ -8,7 +8,8 @@ import {
ComponentType,
type BaseGuildTextChannel,
type ButtonInteraction,
PermissionFlagsBits
PermissionFlagsBits,
MessageFlags
} from "discord.js";
import { userService } from "@/modules/user/user.service";
import { inventoryService } from "@/modules/inventory/inventory.service";
@@ -31,7 +32,7 @@ export const sell = createCommand({
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
await interaction.deferReply({ ephemeral: true });
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const itemId = interaction.options.getNumber("itemid", true);
const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel;
@@ -90,7 +91,7 @@ export const sell = createCommand({
async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof items.$inferSelect) {
try {
await interaction.deferReply({ ephemeral: true });
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const userId = interaction.user.id;
const user = await userService.getUserById(userId);
@@ -118,7 +119,7 @@ async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof
if (interaction.deferred || interaction.replied) {
await interaction.editReply({ content: "", embeds: [createErrorEmbed("An error occurred while processing your purchase.")] });
} else {
await interaction.reply({ embeds: [createErrorEmbed("An error occurred while processing your purchase.")], ephemeral: true });
await interaction.reply({ embeds: [createErrorEmbed("An error occurred while processing your purchase.")], flags: MessageFlags.Ephemeral });
}
}
}

View File

@@ -0,0 +1,13 @@
import { Events } from "discord.js";
import type { Event } from "@lib/types";
const event: Event<Events.GuildMemberAdd> = {
name: Events.GuildMemberAdd,
execute: async (member) => {
const role = member.guild.roles.cache.find(role => role.name === "Visitor");
if (!role) return;
await member.roles.add(role);
},
};
export default event;

View File

@@ -0,0 +1,53 @@
import { Events, MessageFlags } from "discord.js";
import { KyokoClient } from "@lib/KyokoClient";
import { userService } from "@/modules/user/user.service";
import { createErrorEmbed } from "@lib/embeds";
import type { Event } from "@lib/types";
const event: Event<Events.InteractionCreate> = {
name: Events.InteractionCreate,
execute: async (interaction) => {
// Handle Trade Interactions
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
if (interaction.customId.startsWith("trade_") || interaction.customId === "amount") {
await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction));
return;
}
}
if (!interaction.isChatInputCommand()) return;
const command = KyokoClient.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
// Ensure user exists in database
try {
const user = await userService.getUserById(interaction.user.id);
if (!user) {
console.log(`🆕 Creating new user entry for ${interaction.user.tag}`);
await userService.createUser(interaction.user.id, interaction.user.username);
}
} catch (error) {
console.error("Failed to check/create user:", error);
}
try {
await command.execute(interaction);
} catch (error) {
console.error(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 });
}
}
},
};
export default event;

View File

@@ -0,0 +1,19 @@
import { Events } from "discord.js";
import { userService } from "@/modules/user/user.service";
import { levelingService } from "@/modules/leveling/leveling.service";
import type { Event } from "@lib/types";
const event: Event<Events.MessageCreate> = {
name: Events.MessageCreate,
execute: async (message) => {
if (message.author.bot) return;
if (!message.guild) return;
const user = await userService.getUserById(message.author.id);
if (!user) return;
levelingService.processChatXp(message.author.id);
},
};
export default event;

14
src/events/ready.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Events } from "discord.js";
import { schedulerService } from "@/modules/system/scheduler";
import type { Event } from "@lib/types";
const event: Event<Events.ClientReady> = {
name: Events.ClientReady,
once: true,
execute: async (c) => {
console.log(`Ready! Logged in as ${c.user.tag}`);
schedulerService.start();
},
};
export default event;

View File

@@ -1,81 +1,11 @@
import { Events } from "discord.js";
import { KyokoClient } from "@lib/KyokoClient";
import { env } from "@lib/env";
import { userService } from "@/modules/user/user.service";
import { levelingService } from "@/modules/leveling/leveling.service";
import { createErrorEmbed } from "@lib/embeds";
// Load commands
// Load commands & events
await KyokoClient.loadCommands();
await KyokoClient.loadEvents();
await KyokoClient.deployCommands();
import { schedulerService } from "@/modules/system/scheduler";
KyokoClient.once(Events.ClientReady, async c => {
console.log(`Ready! Logged in as ${c.user.tag}`);
schedulerService.start();
});
// give visitor role to new users
KyokoClient.on(Events.GuildMemberAdd, async member => {
const role = member.guild.roles.cache.find(role => role.name === "Visitor");
if (!role) return;
await member.roles.add(role);
});
// process xp on message
KyokoClient.on(Events.MessageCreate, async message => {
if (message.author.bot) return;
if (!message.guild) return;
const user = await userService.getUserById(message.author.id);
if (!user) return;
levelingService.processChatXp(message.author.id);
});
// handle commands
KyokoClient.on(Events.InteractionCreate, async interaction => {
// Handle Trade Interactions
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
if (interaction.customId.startsWith("trade_") || interaction.customId === "amount") {
await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction));
return;
}
}
if (!interaction.isChatInputCommand()) return;
const command = KyokoClient.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
// Ensure user exists in database
try {
const user = await userService.getUserById(interaction.user.id);
if (!user) {
console.log(`🆕 Creating new user entry for ${interaction.user.tag}`);
await userService.createUser(interaction.user.id, interaction.user.username);
}
} catch (error) {
console.error("Failed to check/create user:", error);
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
const errorEmbed = createErrorEmbed('There was an error while executing this command!');
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
} else {
await interaction.reply({ embeds: [errorEmbed], ephemeral: true });
}
}
});
// login with the token from .env
if (!env.DISCORD_BOT_TOKEN) {

View File

@@ -1,7 +1,7 @@
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js";
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import type { Command } from "@lib/types";
import type { Command, Event } from "@lib/types";
import { env } from "@lib/env";
class Client extends DiscordClient {
@@ -23,6 +23,16 @@ class Client extends DiscordClient {
await this.readCommandsRecursively(commandsPath, reload);
}
async loadEvents(reload: boolean = false) {
if (reload) {
this.removeAllListeners();
console.log("♻️ Reloading events...");
}
const eventsPath = join(import.meta.dir, '../events');
await this.readEventsRecursively(eventsPath, reload);
}
private async readCommandsRecursively(dir: string, reload: boolean = false) {
try {
const files = await readdir(dir, { withFileTypes: true });
@@ -63,10 +73,52 @@ class Client extends DiscordClient {
}
}
private async readEventsRecursively(dir: string, reload: boolean = false) {
try {
const files = await readdir(dir, { withFileTypes: true });
for (const file of files) {
const filePath = join(dir, file.name);
if (file.isDirectory()) {
await this.readEventsRecursively(filePath, reload);
continue;
}
if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue;
try {
const importPath = reload ? `${filePath}?t=${Date.now()}` : filePath;
const eventModule = await import(importPath);
const event = eventModule.default;
if (this.isValidEvent(event)) {
if (event.once) {
this.once(event.name, (...args) => event.execute(...args));
} else {
this.on(event.name, (...args) => event.execute(...args));
}
console.log(`✅ Loaded event: ${event.name}`);
} else {
console.warn(`⚠️ Skipping invalid event in ${file.name}`);
}
} catch (error) {
console.error(`❌ Failed to load event from ${filePath}:`, error);
}
}
} catch (error) {
console.error(`Error reading directory ${dir}:`, error);
}
}
private isValidCommand(command: any): command is Command {
return command && typeof command === 'object' && 'data' in command && 'execute' in command;
}
private isValidEvent(event: any): event is Event<any> {
return event && typeof event === 'object' && 'name' in event && 'execute' in event;
}
async deployCommands() {
// We use env.DISCORD_BOT_TOKEN directly so this can run without client.login()
const token = env.DISCORD_BOT_TOKEN;

View File

@@ -1,6 +1,12 @@
import type { ChatInputCommandInteraction, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js";
import type { ChatInputCommandInteraction, ClientEvents, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js";
export interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder;
execute: (interaction: ChatInputCommandInteraction) => Promise<void> | void;
}
export interface Event<K extends keyof ClientEvents> {
name: K;
once?: boolean;
execute: (...args: ClientEvents[K]) => Promise<void> | void;
}