From 41297db1d15ccffc2a79a1544dfc001fc8a2b54f Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 13:37:42 +0700 Subject: [PATCH 01/18] fix: reset grinder role select query --- .../client-ready/jobs/validators/reset-grinder-roles.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts index 287406c..e8d86d0 100644 --- a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts @@ -61,14 +61,6 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { const users = await prisma.user.findMany({ select: { discord_id: true, - checkins: { - select: { - status: true, - created_at: true, - }, - orderBy: { created_at: 'desc' }, - take: 1, - }, checkin_streaks: { orderBy: { first_date: 'desc' }, take: 1, From ec1f94586e464aaa12020baf23de66b1984a2207 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 13:52:46 +0700 Subject: [PATCH 02/18] feat: note button when reset grinder roles --- .../jobs/handlers/reset-grinder-roles.ts | 4 ++++ .../jobs/messages/reset-grinder-roles.ts | 3 ++- .../jobs/validators/reset-grinder-roles.ts | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts index e8a6940..e9e4ed0 100644 --- a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts @@ -2,6 +2,8 @@ import type { Event } from '@events/event' import type { Client } from 'discord.js' import process from 'node:process' import { GRIND_ASHES_CHANNEL } from '@config/discord' +import { EVENT_PATH } from '@events/index' +import { generateCustomId } from '@utils/component' import { getChannel } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' @@ -15,6 +17,8 @@ export class ResetGrinderRolesError extends DiscordBaseError { } } +export const GOODBYE_NOTE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` + export default { name: Events.ClientReady, desc: `Reset Grinder roles for users that didn't do a check-in yesterday or the check-in didn't approved.`, diff --git a/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts index a4119a9..659e3a7 100644 --- a/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts @@ -22,7 +22,8 @@ Api bukanlah padam karena kelemahan, melainkan karena ia tak disirami pada waktu Namun jangan berduka, jalan ini selalu terbuka bagi mereka yang bersedia memulai kembali. Apabila Tuan/Nona berkehendak menyalakan api kembali, silakan kembali ke <#${IGNITE_PATH_CHANNEL}> dan bangkitlah dari awal. *Aksaria menanti mereka yang konsisten.* - + `, + GoodByeNotes: ` > Apabila *check-in* Tuan/Nona masih berada dalam status menunggu peninjauan (*waiting*) dan belum memperoleh keputusan hingga mendekati pergantian hari, maka dengan ini disampaikan ketentuan berikut: > 1. Jangan terlebih dahulu memasuki ⁠<#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan. > 2. Silakan menjalankan perintah **\`/checkin-status\`** untuk menampilkan status *check-in* terakhir Tuan/Nona. diff --git a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts index e8d86d0..c895328 100644 --- a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts @@ -3,12 +3,25 @@ import type { CheckinStreak } from '@type/checkin-streak' import type { User } from '@type/user' import type { Guild, GuildMember, TextChannel } from 'discord.js' import { getGrindRoles, GRINDER_ROLE } from '@config/discord' +import { encodeSnowflake, getCustomId } from '@utils/component' import { isDateToday, isDateYesterday } from '@utils/date' import { sendAsBot } from '@utils/discord' import { log } from '@utils/logger' +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js' +import { GOODBYE_NOTE_BUTTON_ID } from '../handlers/reset-grinder-roles' import { ResetGrinderRolesMessage } from '../messages/reset-grinder-roles' export class ResetGrinderRoles extends ResetGrinderRolesMessage { + static generateButton(guildId: string): ActionRowBuilder { + const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId)]) + const noteButton = new ButtonBuilder() + .setCustomId(noteButtonId) + .setLabel('πŸ“œ Ketentuan Peninjauan Api') + .setStyle(ButtonStyle.Primary) + + return new ActionRowBuilder().addComponents(noteButton) + } + static hasValidCheckin(checkin?: { created_at: Date, status: string }): boolean { if (!checkin) return false @@ -46,11 +59,12 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { const member = await guild.members.fetch(user.discord_id) await this.removeGrinderRoles(member) await this.breakCheckinStreakAt(prisma, checkinStreak) + const button = this.generateButton(guild.id) await sendAsBot( null, channel, - { content: ResetGrinderRoles.MSG.GoodBye(member), allowedMentions: { users: [member.id], roles: [] } }, + { content: ResetGrinderRoles.MSG.GoodBye(member), components: [button], allowedMentions: { users: [member.id], roles: [] } }, ) log.info(this.MSG.RemoveGrinderRoleFrom(member)) From c91d7bdf87b541c05cc60af971f6266abbf2def1 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 14:18:15 +0700 Subject: [PATCH 03/18] fix: last checkin on reset grinder roles --- .../client-ready/jobs/validators/reset-grinder-roles.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts index c895328..c76afe7 100644 --- a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts @@ -48,14 +48,14 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { static async validateUsers(prisma: PrismaClient, guild: Guild, channel: TextChannel, users: User[]) { for (const user of users) { - const lastCheckin = user.checkins?.[0] - if (this.hasValidCheckin(lastCheckin)) - continue - const checkinStreak = user.checkin_streaks?.[0] if (!checkinStreak) continue + const lastCheckin = checkinStreak.checkins?.[0] + if (this.hasValidCheckin(lastCheckin)) + continue + const member = await guild.members.fetch(user.discord_id) await this.removeGrinderRoles(member) await this.breakCheckinStreakAt(prisma, checkinStreak) From 579b1c04a4e00702275ebfb4c2301b3d4571b0e1 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 14:18:29 +0700 Subject: [PATCH 04/18] feat: note button handler --- .../jobs/handlers/reset-grinder-roles.ts | 4 -- .../jobs/messages/reset-grinder-roles.ts | 4 +- .../jobs/validators/reset-grinder-roles.ts | 23 ++++++++-- .../handlers/reset-grinder-roles-button.ts | 45 +++++++++++++++++++ 4 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts diff --git a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts index e9e4ed0..e8a6940 100644 --- a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts @@ -2,8 +2,6 @@ import type { Event } from '@events/event' import type { Client } from 'discord.js' import process from 'node:process' import { GRIND_ASHES_CHANNEL } from '@config/discord' -import { EVENT_PATH } from '@events/index' -import { generateCustomId } from '@utils/component' import { getChannel } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' @@ -17,8 +15,6 @@ export class ResetGrinderRolesError extends DiscordBaseError { } } -export const GOODBYE_NOTE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` - export default { name: Events.ClientReady, desc: `Reset Grinder roles for users that didn't do a check-in yesterday or the check-in didn't approved.`, diff --git a/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts index 659e3a7..a6a6e6a 100644 --- a/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts @@ -1,5 +1,5 @@ import type { GuildMember } from 'discord.js' -import { FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord' +import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord' import { DiscordAssert } from '@utils/discord' export class ResetGrinderRolesMessage extends DiscordAssert { @@ -26,7 +26,7 @@ Namun jangan berduka, jalan ini selalu terbuka bagi mereka yang bersedia memulai GoodByeNotes: ` > Apabila *check-in* Tuan/Nona masih berada dalam status menunggu peninjauan (*waiting*) dan belum memperoleh keputusan hingga mendekati pergantian hari, maka dengan ini disampaikan ketentuan berikut: > 1. Jangan terlebih dahulu memasuki ⁠<#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan. -> 2. Silakan menjalankan perintah **\`/checkin-status\`** untuk menampilkan status *check-in* terakhir Tuan/Nona. +> 2. Silakan menjalankan perintah **\`/checkin-status\`** pada <#${AUDIT_FLAME_CHANNEL}> untuk menampilkan status *check-in* terakhir Tuan/Nona. > 3. Setelah pesan status tersebut muncul, berikan reaksi β€œβ“β€ pada pesan tersebut. > 4. Dari reaksi tersebut, sebuah *thread* akan tercipta secara otomatis sebagai ruang klarifikasi dan komunikasi dengan <@&${FLAMEWARDEN_ROLE}>. > ⏳ Batas waktu penantian atas status WAITING adalah maksimal 1Γ—24 jam sejak *check-in* diajukan. diff --git a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts index c76afe7..75e1017 100644 --- a/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts @@ -1,17 +1,32 @@ import type { PrismaClient } from '@generatedDB/client' import type { CheckinStreak } from '@type/checkin-streak' import type { User } from '@type/user' -import type { Guild, GuildMember, TextChannel } from 'discord.js' +import type { Guild, GuildMember, Interaction, TextChannel } from 'discord.js' import { getGrindRoles, GRINDER_ROLE } from '@config/discord' -import { encodeSnowflake, getCustomId } from '@utils/component' +import { GOODBYE_NOTE_BUTTON_ID, ResetGrinderRolesButtonError } from '@events/interaction-create/jobs/handlers/reset-grinder-roles-button' +import { decodeSnowflakes, encodeSnowflake, getCustomId } from '@utils/component' import { isDateToday, isDateYesterday } from '@utils/date' -import { sendAsBot } from '@utils/discord' +import { DiscordAssert, sendAsBot } from '@utils/discord' import { log } from '@utils/logger' import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js' -import { GOODBYE_NOTE_BUTTON_ID } from '../handlers/reset-grinder-roles' import { ResetGrinderRolesMessage } from '../messages/reset-grinder-roles' export class ResetGrinderRoles extends ResetGrinderRolesMessage { + static override BASE_PERMS = [ + ...DiscordAssert.BASE_PERMS, + ] + + static getButtonId(interaction: Interaction, customId: string) { + const [prefix, guildId] = decodeSnowflakes(customId) + + if (!guildId) + throw new ResetGrinderRolesButtonError(this.ERR.GuildMissing) + if (interaction.guildId !== guildId) + throw new ResetGrinderRolesButtonError(this.ERR.NotGuild) + + return { prefix, guildId } + } + static generateButton(guildId: string): ActionRowBuilder { const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId)]) const noteButton = new ButtonBuilder() diff --git a/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts b/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts new file mode 100644 index 0000000..b16356e --- /dev/null +++ b/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts @@ -0,0 +1,45 @@ +import type { Event } from '@events/event' +import type { Interaction, TextChannel } from 'discord.js' +import { ResetGrinderRoles } from '@events/client-ready/jobs/validators/reset-grinder-roles' +import { EVENT_PATH } from '@events/index' +import { generateCustomId } from '@utils/component' +import { sendReply } from '@utils/discord' +import { DiscordBaseError } from '@utils/discord/error' +import { log } from '@utils/logger' +import { Events } from 'discord.js' + +export class ResetGrinderRolesButtonError extends DiscordBaseError { + constructor(message: string, options?: { cause?: unknown }) { + super('ResetGrinderRolesButtonError', message, options) + } +} + +export const GOODBYE_NOTE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` + +export default { + name: Events.InteractionCreate, + desc: 'Opens goodbye note modal for users losing Grinder roles.', + async exec(_, interaction: Interaction) { + if (!interaction.isButton()) + return + + const isValid = ResetGrinderRoles.assertComponentId(interaction.customId, GOODBYE_NOTE_BUTTON_ID) + if (!isValid) + return + + try { + if (!interaction.inCachedGuild()) + throw new ResetGrinderRolesButtonError(ResetGrinderRoles.ERR.NotGuild) + + const channel = interaction.channel as TextChannel + ResetGrinderRoles.assertMissPerms(interaction.client.user, channel) + + await sendReply(interaction, ResetGrinderRoles.MSG.GoodByeNotes) + } + catch (err: any) { + if (err instanceof DiscordBaseError) + await sendReply(interaction, err.message) + else log.error(`Failed to handle ${GOODBYE_NOTE_BUTTON_ID}: ${ResetGrinderRoles.ERR.UnexpectedButton}: ${err}`) + } + }, +} as Event From 083538f01ecc18296a6871d1f6cc61124f369269 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 19:45:16 +0700 Subject: [PATCH 05/18] refactor: `interaction-create` event --- .../checkin/handlers/checkin-audit.ts | 4 +-- src/bot/commands/checkin/handlers/checkin.ts | 4 +-- .../checkin/validators/checkin-status.ts | 2 +- src/bot/commands/index.ts | 8 +++-- .../handlers/{say-hello.ts => index.ts} | 2 +- .../messages/{say-hello.ts => index.ts} | 0 .../say-hello/validators/index.ts | 4 +++ .../say-hello/validators/say-hello.ts | 4 --- .../handlers/{grinder-role.ts => index.ts} | 2 +- .../messages/{grinder-role.ts => index.ts} | 0 .../validators/{grinder-role.ts => index.ts} | 2 +- src/bot/events/index.ts | 18 +++++----- ...in-approve-button.ts => approve-button.ts} | 25 ++++++------- ...{checkin-audit-modal.ts => audit-modal.ts} | 25 ++++++------- ...button-modal.ts => custom-button-modal.ts} | 23 +++++------- ...ckin-custom-button.ts => custom-button.ts} | 25 ++++++------- .../handlers/{checkin-modal.ts => modal.ts} | 23 +++++------- ...ckin-reject-button.ts => reject-button.ts} | 23 +++++------- .../messages/{checkin-audit.ts => audit.ts} | 0 .../checkin/messages/{checkin.ts => index.ts} | 0 .../validators/{checkin-audit.ts => audit.ts} | 6 ++-- .../validators/{checkin.ts => index.ts} | 16 ++++----- .../handlers/role-grant-create-button.ts | 21 +++++------ .../embed/handlers/role-grant-create-modal.ts | 21 +++++------ src/bot/events/interaction-create/entry.ts | 29 +++++++++++++++ .../interaction-create/execute-command.ts | 31 ---------------- .../execute/handlers/command.ts | 35 +++++++++++++++++++ .../execute/messages/command.ts | 9 +++++ .../execute/validators/command.ts | 22 ++++++++++++ .../handlers/reset-grinder-roles-button.ts | 21 +++++------ .../message/handlers/send-modal.ts | 23 +++++------- src/bot/events/interaction-create/registry.ts | 15 ++++++++ .../im-fine/handlers/{im-fine.ts => index.ts} | 0 .../im-fine/messages/{im-fine.ts => index.ts} | 0 .../im-fine/validators/im-fine.ts | 4 --- .../im-fine/validators/index.ts | 4 +++ .../{submitted-checkin.ts => submitted.ts} | 2 +- src/config/discord.ts | 3 +- src/utils/discord/assert.ts | 7 ---- src/utils/io.ts | 2 +- 40 files changed, 246 insertions(+), 219 deletions(-) rename src/bot/events/client-ready/say-hello/handlers/{say-hello.ts => index.ts} (93%) rename src/bot/events/client-ready/say-hello/messages/{say-hello.ts => index.ts} (100%) create mode 100644 src/bot/events/client-ready/say-hello/validators/index.ts delete mode 100644 src/bot/events/client-ready/say-hello/validators/say-hello.ts rename src/bot/events/guild-member-update/grinder-role/handlers/{grinder-role.ts => index.ts} (96%) rename src/bot/events/guild-member-update/grinder-role/messages/{grinder-role.ts => index.ts} (100%) rename src/bot/events/guild-member-update/grinder-role/validators/{grinder-role.ts => index.ts} (74%) rename src/bot/events/interaction-create/checkin/handlers/{checkin-approve-button.ts => approve-button.ts} (69%) rename src/bot/events/interaction-create/checkin/handlers/{checkin-audit-modal.ts => audit-modal.ts} (77%) rename src/bot/events/interaction-create/checkin/handlers/{checkin-custom-button-modal.ts => custom-button-modal.ts} (76%) rename src/bot/events/interaction-create/checkin/handlers/{checkin-custom-button.ts => custom-button.ts} (73%) rename src/bot/events/interaction-create/checkin/handlers/{checkin-modal.ts => modal.ts} (82%) rename src/bot/events/interaction-create/checkin/handlers/{checkin-reject-button.ts => reject-button.ts} (73%) rename src/bot/events/interaction-create/checkin/messages/{checkin-audit.ts => audit.ts} (100%) rename src/bot/events/interaction-create/checkin/messages/{checkin.ts => index.ts} (100%) rename src/bot/events/interaction-create/checkin/validators/{checkin-audit.ts => audit.ts} (95%) rename src/bot/events/interaction-create/checkin/validators/{checkin.ts => index.ts} (97%) create mode 100644 src/bot/events/interaction-create/entry.ts delete mode 100644 src/bot/events/interaction-create/execute-command.ts create mode 100644 src/bot/events/interaction-create/execute/handlers/command.ts create mode 100644 src/bot/events/interaction-create/execute/messages/command.ts create mode 100644 src/bot/events/interaction-create/execute/validators/command.ts create mode 100644 src/bot/events/interaction-create/registry.ts rename src/bot/events/message-create/im-fine/handlers/{im-fine.ts => index.ts} (100%) rename src/bot/events/message-create/im-fine/messages/{im-fine.ts => index.ts} (100%) delete mode 100644 src/bot/events/message-create/im-fine/validators/im-fine.ts create mode 100644 src/bot/events/message-create/im-fine/validators/index.ts rename src/bot/events/message-reaction-add/checkin/handlers/{submitted-checkin.ts => submitted.ts} (99%) diff --git a/src/bot/commands/checkin/handlers/checkin-audit.ts b/src/bot/commands/checkin/handlers/checkin-audit.ts index 85bf8cf..10f1afc 100644 --- a/src/bot/commands/checkin/handlers/checkin-audit.ts +++ b/src/bot/commands/checkin/handlers/checkin-audit.ts @@ -1,13 +1,13 @@ import type { Command } from '@commands/command' import type { ChatInputCommandInteraction, Client } from 'discord.js' import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' -import { CHECKIN_AUDIT_ID } from '@events/interaction-create/checkin/handlers/checkin-audit-modal' +import { CHECKIN_AUDIT_ID } from '@events/interaction-create/checkin/handlers/audit-modal' import { createCheckinReviewModal, encodeSnowflake, getCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' import { SlashCommandBuilder } from 'discord.js' -import { CheckinAudit } from '../../../events/interaction-create/checkin/validators/checkin-audit' +import { CheckinAudit } from '../../../events/interaction-create/checkin/validators/audit' export class CheckinAuditError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { diff --git a/src/bot/commands/checkin/handlers/checkin.ts b/src/bot/commands/checkin/handlers/checkin.ts index 432dc16..ab96010 100644 --- a/src/bot/commands/checkin/handlers/checkin.ts +++ b/src/bot/commands/checkin/handlers/checkin.ts @@ -1,8 +1,8 @@ import type { Command } from '@commands/command' import type { ChatInputCommandInteraction } from 'discord.js' import { CHECKIN_CHANNEL } from '@config/discord' -import { CHECKIN_ID } from '@events/interaction-create/checkin/handlers/checkin-modal' -import { Checkin } from '@events/interaction-create/checkin/validators/checkin' +import { CHECKIN_ID } from '@events/interaction-create/checkin/handlers/modal' +import { Checkin } from '@events/interaction-create/checkin/validators' import { encodeSnowflake, getCustomId } from '@utils/component' import { getAttachments, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' diff --git a/src/bot/commands/checkin/validators/checkin-status.ts b/src/bot/commands/checkin/validators/checkin-status.ts index 102e750..f77da52 100644 --- a/src/bot/commands/checkin/validators/checkin-status.ts +++ b/src/bot/commands/checkin/validators/checkin-status.ts @@ -3,7 +3,7 @@ import type { Checkin as CheckinType } from '@type/checkin' import type { User } from '@type/user' import type { EmbedBuilder, Guild } from 'discord.js' import { FLAMEWARDEN_ROLE } from '@config/discord' -import { Checkin } from '@events/interaction-create/checkin/validators/checkin' +import { Checkin } from '@events/interaction-create/checkin/validators' import { createEmbed } from '@utils/component' import { DiscordAssert } from '@utils/discord' import { DUMMY } from '@utils/placeholder' diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index 8a897f7..06cf1c8 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -20,12 +20,14 @@ export async function registerCommands(client: Client) { client.commands = new Collection() for (const file of files) { + const { default: command } = await import(file) as { default: Command } const fileName = getModuleName(COMMAND_PATH, file) - log.info(`Registering command ${fileName}...`) try { - const { default: command } = await import(file) as { default: Command } - client.commands.set(command.data.name, command) + if (command) { + log.info(`Registering command ${fileName}...`) + client.commands.set(command.data.name, command) + } } catch (err: any) { const msg = err instanceof CommandError ? err.message : '❌ Something went wrong when importing the command' diff --git a/src/bot/events/client-ready/say-hello/handlers/say-hello.ts b/src/bot/events/client-ready/say-hello/handlers/index.ts similarity index 93% rename from src/bot/events/client-ready/say-hello/handlers/say-hello.ts rename to src/bot/events/client-ready/say-hello/handlers/index.ts index b7add49..90b6e74 100644 --- a/src/bot/events/client-ready/say-hello/handlers/say-hello.ts +++ b/src/bot/events/client-ready/say-hello/handlers/index.ts @@ -3,7 +3,7 @@ import type { Client } from 'discord.js' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' import { Events } from 'discord.js' -import { SayHello } from '../validators/say-hello' +import { SayHello } from '../validators' export class SayHelloError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { diff --git a/src/bot/events/client-ready/say-hello/messages/say-hello.ts b/src/bot/events/client-ready/say-hello/messages/index.ts similarity index 100% rename from src/bot/events/client-ready/say-hello/messages/say-hello.ts rename to src/bot/events/client-ready/say-hello/messages/index.ts diff --git a/src/bot/events/client-ready/say-hello/validators/index.ts b/src/bot/events/client-ready/say-hello/validators/index.ts new file mode 100644 index 0000000..d4d8ce2 --- /dev/null +++ b/src/bot/events/client-ready/say-hello/validators/index.ts @@ -0,0 +1,4 @@ +import { SayHelloMessage } from '../messages' + +export class SayHello extends SayHelloMessage { +} diff --git a/src/bot/events/client-ready/say-hello/validators/say-hello.ts b/src/bot/events/client-ready/say-hello/validators/say-hello.ts deleted file mode 100644 index 68db5c8..0000000 --- a/src/bot/events/client-ready/say-hello/validators/say-hello.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SayHelloMessage } from '../messages/say-hello' - -export class SayHello extends SayHelloMessage { -} diff --git a/src/bot/events/guild-member-update/grinder-role/handlers/grinder-role.ts b/src/bot/events/guild-member-update/grinder-role/handlers/index.ts similarity index 96% rename from src/bot/events/guild-member-update/grinder-role/handlers/grinder-role.ts rename to src/bot/events/guild-member-update/grinder-role/handlers/index.ts index 078f7cb..fc8bea1 100644 --- a/src/bot/events/guild-member-update/grinder-role/handlers/grinder-role.ts +++ b/src/bot/events/guild-member-update/grinder-role/handlers/index.ts @@ -5,7 +5,7 @@ import { getChannel, sendAsBot } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' import { Events } from 'discord.js' -import { GrinderRole } from '../validators/grinder-role' +import { GrinderRole } from '../validators' export class GrinderRoleError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { diff --git a/src/bot/events/guild-member-update/grinder-role/messages/grinder-role.ts b/src/bot/events/guild-member-update/grinder-role/messages/index.ts similarity index 100% rename from src/bot/events/guild-member-update/grinder-role/messages/grinder-role.ts rename to src/bot/events/guild-member-update/grinder-role/messages/index.ts diff --git a/src/bot/events/guild-member-update/grinder-role/validators/grinder-role.ts b/src/bot/events/guild-member-update/grinder-role/validators/index.ts similarity index 74% rename from src/bot/events/guild-member-update/grinder-role/validators/grinder-role.ts rename to src/bot/events/guild-member-update/grinder-role/validators/index.ts index 61330ae..9fcde8b 100644 --- a/src/bot/events/guild-member-update/grinder-role/validators/grinder-role.ts +++ b/src/bot/events/guild-member-update/grinder-role/validators/index.ts @@ -1,5 +1,5 @@ import { DiscordAssert } from '@utils/discord' -import { GrinderRoleMessage } from '../messages/grinder-role' +import { GrinderRoleMessage } from '../messages' export class GrinderRole extends GrinderRoleMessage { static override BASE_PERMS = [ diff --git a/src/bot/events/index.ts b/src/bot/events/index.ts index 817cda5..b1de852 100644 --- a/src/bot/events/index.ts +++ b/src/bot/events/index.ts @@ -19,16 +19,18 @@ export async function registerEvents(client: Client) { for (const file of files) { const { default: event } = await import(file) as { default: Event } const fileName = getModuleName(EVENT_PATH, file) - log.info(`Registering event ${fileName}...`) try { - if (event.once) { - client.once(event.name, (...args) => event.exec(client, ...args)) - } - else { - client.on(event.name, (...args) => { - event.exec(client, ...args) - }) + if (event) { + log.info(`Registering event ${fileName}...`) + if (event.once) { + client.once(event.name, (...args) => event.exec(client, ...args)) + } + else { + client.on(event.name, (...args) => { + event.exec(client, ...args) + }) + } } } catch (err: any) { diff --git a/src/bot/events/interaction-create/checkin/handlers/checkin-approve-button.ts b/src/bot/events/interaction-create/checkin/handlers/approve-button.ts similarity index 69% rename from src/bot/events/interaction-create/checkin/handlers/checkin-approve-button.ts rename to src/bot/events/interaction-create/checkin/handlers/approve-button.ts index 29c0199..3a7522e 100644 --- a/src/bot/events/interaction-create/checkin/handlers/checkin-approve-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/approve-button.ts @@ -1,13 +1,11 @@ -import type { Event } from '@events/event' -import type { Client, Interaction, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' -import { Checkin } from '../validators/checkin' +import { Checkin } from '../validators' export class CheckinApproveButtonError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -17,17 +15,14 @@ export class CheckinApproveButtonError extends DiscordBaseError { export const CHECKIN_APPROVE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` -export default { - name: Events.InteractionCreate, - desc: 'Handles check-in approve button interactions and approves user check-in.', - async exec(client: Client, interaction: Interaction) { +registerInteractionHandler({ + desc: 'Approves a user check-in from the approve button.', + id: CHECKIN_APPROVE_BUTTON_ID, + errorTag: () => `${CHECKIN_APPROVE_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}`, + async exec(client, interaction) { if (!interaction.isButton()) return - const isValidComponent = Checkin.assertComponentId(interaction.customId, CHECKIN_APPROVE_BUTTON_ID) - if (!isValidComponent) - return - try { await interaction.deferUpdate() @@ -54,7 +49,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${CHECKIN_APPROVE_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/checkin/handlers/checkin-audit-modal.ts b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts similarity index 77% rename from src/bot/events/interaction-create/checkin/handlers/checkin-audit-modal.ts rename to src/bot/events/interaction-create/checkin/handlers/audit-modal.ts index 657b2d2..80ccaee 100644 --- a/src/bot/events/interaction-create/checkin/handlers/checkin-audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -1,15 +1,13 @@ -import type { Event } from '@events/event' import type { CheckinStatusType } from '@type/checkin' -import type { Client, Interaction, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' -import { Checkin } from '../validators/checkin' -import { CheckinAudit } from '../validators/checkin-audit' +import { Checkin } from '../validators' +import { CheckinAudit } from '../validators/audit' export class CheckinAuditModalError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -19,17 +17,14 @@ export class CheckinAuditModalError extends DiscordBaseError { export const CHECKIN_AUDIT_ID = generateCustomId(EVENT_PATH, __filename) -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Handles modal submissions for check-in audit modal forms.', - async exec(client: Client, interaction: Interaction) { + id: CHECKIN_AUDIT_ID, + errorTag: () => `${CHECKIN_AUDIT_ID}: ${CheckinAudit.ERR.UnexpectedModal}`, + async exec(client, interaction) { if (!interaction.isModalSubmit()) return - const isValidComponent = CheckinAudit.assertComponentId(interaction.customId, CHECKIN_AUDIT_ID) - if (!isValidComponent) - return - try { await interaction.deferUpdate() @@ -59,7 +54,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${CHECKIN_AUDIT_ID}: ${CheckinAudit.ERR.UnexpectedModal}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/checkin/handlers/checkin-custom-button-modal.ts b/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts similarity index 76% rename from src/bot/events/interaction-create/checkin/handlers/checkin-custom-button-modal.ts rename to src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts index 46363cc..5dba028 100644 --- a/src/bot/events/interaction-create/checkin/handlers/checkin-custom-button-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts @@ -1,14 +1,12 @@ -import type { Event } from '@events/event' import type { CheckinStatusType } from '@type/checkin' -import type { Client, Interaction, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' -import { Checkin } from '../validators/checkin' +import { Checkin } from '../validators' export class CheckinCustomButtonModalError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -18,17 +16,14 @@ export class CheckinCustomButtonModalError extends DiscordBaseError { export const CHECKIN_CUSTOM_BUTTON_MODAL_ID = generateCustomId(EVENT_PATH, __filename) -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Handles modal submissions for the custom check-in review modal.', - async exec(client: Client, interaction: Interaction) { + id: CHECKIN_CUSTOM_BUTTON_MODAL_ID, + errorTag: () => `${CHECKIN_CUSTOM_BUTTON_MODAL_ID}: ${Checkin.ERR.UnexpectedModal}`, + async exec(client, interaction) { if (!interaction.isModalSubmit()) return - const isValidComponent = Checkin.assertComponentId(interaction.customId, CHECKIN_CUSTOM_BUTTON_MODAL_ID) - if (!isValidComponent) - return - try { await interaction.deferUpdate() @@ -60,7 +55,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${CHECKIN_CUSTOM_BUTTON_MODAL_ID}: ${Checkin.ERR.UnexpectedModal}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/checkin/handlers/checkin-custom-button.ts b/src/bot/events/interaction-create/checkin/handlers/custom-button.ts similarity index 73% rename from src/bot/events/interaction-create/checkin/handlers/checkin-custom-button.ts rename to src/bot/events/interaction-create/checkin/handlers/custom-button.ts index f75b367..636ab94 100644 --- a/src/bot/events/interaction-create/checkin/handlers/checkin-custom-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button.ts @@ -1,14 +1,12 @@ -import type { Event } from '@events/event' -import type { Client, Interaction, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { createCheckinReviewModal, encodeSnowflake, generateCustomId, getCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' -import { Checkin } from '../validators/checkin' -import { CHECKIN_CUSTOM_BUTTON_MODAL_ID } from './checkin-custom-button-modal' +import { Checkin } from '../validators' +import { CHECKIN_CUSTOM_BUTTON_MODAL_ID } from './custom-button-modal' export class CheckinCustomButtonError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -18,17 +16,14 @@ export class CheckinCustomButtonError extends DiscordBaseError { export const CHECKIN_CUSTOM_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Opens review modal for a check-in', - async exec(client: Client, interaction: Interaction) { + id: CHECKIN_CUSTOM_BUTTON_ID, + errorTag: () => `${CHECKIN_CUSTOM_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}`, + async exec(client, interaction) { if (!interaction.isButton()) return - const isValid = Checkin.assertComponentId(interaction.customId, CHECKIN_CUSTOM_BUTTON_ID) - if (!isValid) - return - try { if (!interaction.inCachedGuild()) throw new CheckinCustomButtonError(Checkin.ERR.NotGuild) @@ -54,7 +49,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${CHECKIN_CUSTOM_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/checkin/handlers/checkin-modal.ts b/src/bot/events/interaction-create/checkin/handlers/modal.ts similarity index 82% rename from src/bot/events/interaction-create/checkin/handlers/checkin-modal.ts rename to src/bot/events/interaction-create/checkin/handlers/modal.ts index 14db89d..1ca0a8a 100644 --- a/src/bot/events/interaction-create/checkin/handlers/checkin-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/modal.ts @@ -1,13 +1,11 @@ -import type { Event } from '@events/event' -import type { Attachment, Client, GuildMember, Interaction, Message } from 'discord.js' +import type { Attachment, GuildMember, Message } from 'discord.js' import { FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId, tempStore } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' -import { Checkin } from '../validators/checkin' +import { Checkin } from '../validators' export class CheckinModalError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -17,17 +15,14 @@ export class CheckinModalError extends DiscordBaseError { export const CHECKIN_ID = generateCustomId(EVENT_PATH, __filename) -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Handles modal submissions for check-in modal forms.', - async exec(client: Client, interaction: Interaction) { + id: CHECKIN_ID, + errorTag: () => `${CHECKIN_ID}: ${Checkin.ERR.UnexpectedModal}`, + async exec(client, interaction) { if (!interaction.isModalSubmit()) return - const isValidComponent = Checkin.assertComponentId(interaction.customId, CHECKIN_ID) - if (!isValidComponent) - return - try { if (!interaction.inCachedGuild()) throw new CheckinModalError(Checkin.ERR.NotGuild) @@ -81,7 +76,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${CHECKIN_ID}: ${Checkin.ERR.UnexpectedModal}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/checkin/handlers/checkin-reject-button.ts b/src/bot/events/interaction-create/checkin/handlers/reject-button.ts similarity index 73% rename from src/bot/events/interaction-create/checkin/handlers/checkin-reject-button.ts rename to src/bot/events/interaction-create/checkin/handlers/reject-button.ts index 0a728da..10884e6 100644 --- a/src/bot/events/interaction-create/checkin/handlers/checkin-reject-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/reject-button.ts @@ -1,13 +1,11 @@ -import type { Event } from '@events/event' -import type { Client, Interaction, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' -import { Checkin } from '../validators/checkin' +import { Checkin } from '../validators' export class CheckinRejectButtonError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -17,17 +15,14 @@ export class CheckinRejectButtonError extends DiscordBaseError { export const CHECKIN_REJECT_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Handles check-in reject button interactions and rejects user check-in.', - async exec(client: Client, interaction: Interaction) { + id: CHECKIN_REJECT_BUTTON_ID, + errorTag: () => `${CHECKIN_REJECT_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}`, + async exec(client, interaction) { if (!interaction.isButton()) return - const isValidComponent = Checkin.assertComponentId(interaction.customId, CHECKIN_REJECT_BUTTON_ID) - if (!isValidComponent) - return - try { await interaction.deferUpdate() @@ -54,7 +49,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${CHECKIN_REJECT_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/checkin/messages/checkin-audit.ts b/src/bot/events/interaction-create/checkin/messages/audit.ts similarity index 100% rename from src/bot/events/interaction-create/checkin/messages/checkin-audit.ts rename to src/bot/events/interaction-create/checkin/messages/audit.ts diff --git a/src/bot/events/interaction-create/checkin/messages/checkin.ts b/src/bot/events/interaction-create/checkin/messages/index.ts similarity index 100% rename from src/bot/events/interaction-create/checkin/messages/checkin.ts rename to src/bot/events/interaction-create/checkin/messages/index.ts diff --git a/src/bot/events/interaction-create/checkin/validators/checkin-audit.ts b/src/bot/events/interaction-create/checkin/validators/audit.ts similarity index 95% rename from src/bot/events/interaction-create/checkin/validators/checkin-audit.ts rename to src/bot/events/interaction-create/checkin/validators/audit.ts index efea943..6881cdd 100644 --- a/src/bot/events/interaction-create/checkin/validators/checkin-audit.ts +++ b/src/bot/events/interaction-create/checkin/validators/audit.ts @@ -7,9 +7,9 @@ import { decodeSnowflakes } from '@utils/component' import { isDateToday } from '@utils/date' import { DiscordAssert } from '@utils/discord' import { PermissionsBitField } from 'discord.js' -import { CheckinAuditModalError } from '../handlers/checkin-audit-modal' -import { CheckinAuditMessage } from '../messages/checkin-audit' -import { Checkin } from './checkin' +import { CheckinAuditModalError } from '../handlers/audit-modal' +import { CheckinAuditMessage } from '../messages/audit' +import { Checkin } from '.' export class CheckinAudit extends CheckinAuditMessage { static override BASE_PERMS = [ diff --git a/src/bot/events/interaction-create/checkin/validators/checkin.ts b/src/bot/events/interaction-create/checkin/validators/index.ts similarity index 97% rename from src/bot/events/interaction-create/checkin/validators/checkin.ts rename to src/bot/events/interaction-create/checkin/validators/index.ts index 3c4244e..b0c56ed 100644 --- a/src/bot/events/interaction-create/checkin/validators/checkin.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -8,19 +8,19 @@ import type { Attachment, EmbedBuilder, Guild, GuildMember, Interaction, Message import crypto from 'node:crypto' import { CheckinError } from '@commands/checkin/handlers/checkin' import { AURA_FARMING_CHANNEL, CHECKIN_CHANNEL, GRINDER_ROLE } from '@config/discord' -import { SubmittedCheckinError } from '@events/message-reaction-add/checkin/handlers/submitted-checkin' +import { SubmittedCheckinError } from '@events/message-reaction-add/checkin/handlers/submitted' import { createEmbed, decodeSnowflakes, encodeSnowflake, getCustomId } from '@utils/component' import { isDateToday, isDateYesterday } from '@utils/date' import { DiscordAssert, getChannel, sendAsBot } from '@utils/discord' import { attachNewGrindRole, getGrindRoleByStreakCount } from '@utils/discord/roles' import { DUMMY } from '@utils/placeholder' import { ActionRowBuilder, ButtonBuilder, ButtonStyle, messageLink, PermissionsBitField } from 'discord.js' -import { CHECKIN_APPROVE_BUTTON_ID } from '../handlers/checkin-approve-button' -import { CHECKIN_CUSTOM_BUTTON_ID } from '../handlers/checkin-custom-button' -import { CheckinCustomButtonModalError } from '../handlers/checkin-custom-button-modal' -import { CheckinModalError } from '../handlers/checkin-modal' -import { CHECKIN_REJECT_BUTTON_ID } from '../handlers/checkin-reject-button' -import { CheckinMessage } from '../messages/checkin' +import { CHECKIN_APPROVE_BUTTON_ID } from '../handlers/approve-button' +import { CHECKIN_CUSTOM_BUTTON_ID } from '../handlers/custom-button' +import { CheckinCustomButtonModalError } from '../handlers/custom-button-modal' +import { CheckinModalError } from '../handlers/modal' +import { CHECKIN_REJECT_BUTTON_ID } from '../handlers/reject-button' +import { CheckinMessage } from '../messages' export class Checkin extends CheckinMessage { static override BASE_PERMS = [ @@ -228,7 +228,7 @@ export class Checkin extends CheckinMessage { if (!checkin) throw new SubmittedCheckinError(this.ERR.PlainMessage) - await Checkin.setAttachments(prisma, checkin) + await this.setAttachments(prisma, checkin) return checkin } diff --git a/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts b/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts index 3db3a53..269e762 100644 --- a/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts +++ b/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts @@ -1,11 +1,9 @@ -import type { Event } from '@events/event' -import type { GuildMember, Interaction, TextChannel } from 'discord.js' +import type { GuildMember, TextChannel } from 'discord.js' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { getRole, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' import { RoleGrantCreate } from '../validators/role-grant-create' export class EmbedRoleGrantButtonError extends DiscordBaseError { @@ -16,17 +14,14 @@ export class EmbedRoleGrantButtonError extends DiscordBaseError { export const EMBED_ROLE_GRANT_CREATE_BUTTON_ID = generateCustomId(EVENT_PATH, __filename) -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Handles role assignment button interactions and adds a role for users.', - async exec(_, interaction: Interaction) { + id: EMBED_ROLE_GRANT_CREATE_BUTTON_ID, + errorTag: () => `${EMBED_ROLE_GRANT_CREATE_BUTTON_ID}: ${RoleGrantCreate.ERR.UnexpectedButton}`, + async exec(_, interaction) { if (!interaction.isButton()) return - const isValidComponent = RoleGrantCreate.assertComponentId(interaction.customId, EMBED_ROLE_GRANT_CREATE_BUTTON_ID) - if (!isValidComponent) - return - try { if (!interaction.inCachedGuild()) throw new EmbedRoleGrantButtonError(RoleGrantCreate.ERR.NotGuild) @@ -49,7 +44,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${EMBED_ROLE_GRANT_CREATE_BUTTON_ID}: ${RoleGrantCreate.ERR.UnexpectedButton}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts b/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts index 099683f..94ffed5 100644 --- a/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts +++ b/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts @@ -1,11 +1,9 @@ -import type { Event } from '@events/event' -import type { Interaction } from 'discord.js' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { createEmbed, encodeSnowflake, generateCustomId, getCustomId } from '@utils/component' import { getChannel, getRole, sendAsBot, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Events } from 'discord.js' +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js' import { RoleGrantCreate } from '../validators/role-grant-create' import { EMBED_ROLE_GRANT_CREATE_BUTTON_ID } from './role-grant-create-button' @@ -17,17 +15,14 @@ export class EmbedRoleGrantModalError extends DiscordBaseError { export const EMBED_ROLE_GRANT_CREATE_MODAL_ID = generateCustomId(EVENT_PATH, __filename) -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Handles modal submissions for creating an embed with a role-grant button.', - async exec(_, interaction: Interaction) { + id: EMBED_ROLE_GRANT_CREATE_MODAL_ID, + errorTag: () => `${EMBED_ROLE_GRANT_CREATE_MODAL_ID}: ${RoleGrantCreate.ERR.UnexpectedModal}`, + async exec(_, interaction) { if (!interaction.isModalSubmit()) return - const isValidComponent = RoleGrantCreate.assertComponentId(interaction.customId, EMBED_ROLE_GRANT_CREATE_MODAL_ID) - if (!isValidComponent) - return - try { if (!interaction.inCachedGuild()) throw new EmbedRoleGrantModalError(RoleGrantCreate.ERR.NotGuild) @@ -68,7 +63,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${EMBED_ROLE_GRANT_CREATE_MODAL_ID}: ${RoleGrantCreate.ERR.UnexpectedModal}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/entry.ts b/src/bot/events/interaction-create/entry.ts new file mode 100644 index 0000000..12b2530 --- /dev/null +++ b/src/bot/events/interaction-create/entry.ts @@ -0,0 +1,29 @@ +import type { Event } from '@events/event' +import type { Interaction } from 'discord.js' +import { ARCHFYRE_ROLE } from '@config/discord' +import { decodeSnowflakes } from '@utils/component' +import { sendReply } from '@utils/discord' +import { log } from '@utils/logger' +import { Events } from 'discord.js' +import { interactionHandlers } from './registry' + +export default { + name: Events.InteractionCreate, + desc: 'Handles Discord InteractionCreate events and delegates them to registered handlers.', + async exec(client, interaction: Interaction) { + if ('customId' in interaction && interaction.customId) { + const [prefix] = decodeSnowflakes(interaction.customId) + + const handler = interactionHandlers.get(prefix) + if (handler) { + try { + await handler.exec(client, interaction) + } + catch (err) { + await sendReply(interaction, `❓ Something weird happen... kindly contact <@&${ARCHFYRE_ROLE}> :)`) + log.error(`Interaction handler failed ${handler.errorTag()}: ${err}`) + } + } + } + }, +} as Event diff --git a/src/bot/events/interaction-create/execute-command.ts b/src/bot/events/interaction-create/execute-command.ts deleted file mode 100644 index 83484a7..0000000 --- a/src/bot/events/interaction-create/execute-command.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Command } from '@commands/command' -import type { Event } from '@events/event' -import type { Client, Interaction } from 'discord.js' -import { Events, MessageFlags } from 'discord.js' - -export default { - name: Events.InteractionCreate, - desc: 'Executing a command when an interaction is created.', - async exec(client: Client, interaction: Interaction) { - if (!interaction.isChatInputCommand()) - return - const command: Command | undefined = interaction.client.commands.get(interaction.commandName) - if (!command) { - console.error(`No command matching ${interaction.commandName} was found.`) - return - } - - try { - await command.execute(client, interaction) - } - catch (error) { - console.error(error) - if (interaction.replied || interaction.deferred) { - await interaction.followUp({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }) - } - else { - await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }) - } - } - }, -} as Event diff --git a/src/bot/events/interaction-create/execute/handlers/command.ts b/src/bot/events/interaction-create/execute/handlers/command.ts new file mode 100644 index 0000000..9052453 --- /dev/null +++ b/src/bot/events/interaction-create/execute/handlers/command.ts @@ -0,0 +1,35 @@ +import type { Command } from '@commands/command' +import { generateCustomId } from '@utils/component' +import { sendReply } from '@utils/discord' +import { DiscordBaseError } from '@utils/discord/error' +import { EVENT_PATH } from '../../..' +import { registerInteractionHandler } from '../../registry' +import { ExecuteCommand } from '../validators/command' + +export class ExecuteCommandError extends DiscordBaseError { + constructor(message: string, options?: { cause?: unknown }) { + super('ExecuteCommandError', message, options) + } +} + +export const EXECUTE_COMMAND_ID = `${generateCustomId(EVENT_PATH, __filename)}` + +registerInteractionHandler({ + desc: 'Executing a command when an interaction is created.', + id: EXECUTE_COMMAND_ID, + errorTag: () => `${EXECUTE_COMMAND_ID}`, + async exec(client, interaction) { + if (!interaction.isChatInputCommand()) + return + + try { + const command: Command = ExecuteCommand.getCommand(interaction) + await command.execute(client, interaction) + } + catch (err: any) { + if (err instanceof DiscordBaseError) + await sendReply(interaction, err.message) + else throw err + } + }, +}) diff --git a/src/bot/events/interaction-create/execute/messages/command.ts b/src/bot/events/interaction-create/execute/messages/command.ts new file mode 100644 index 0000000..4bc5cfe --- /dev/null +++ b/src/bot/events/interaction-create/execute/messages/command.ts @@ -0,0 +1,9 @@ +import { DiscordAssert } from '@utils/discord' + +export class ExecuteCommandMessage extends DiscordAssert { + static override readonly ERR = { + ...DiscordAssert.ERR, + NoMatchingCommand: (commandName: string) => `❌ No command matching ${commandName} was found`, + UnexpectedExecuteCommand: '❌ Something went wrong during execute command', + } +} diff --git a/src/bot/events/interaction-create/execute/validators/command.ts b/src/bot/events/interaction-create/execute/validators/command.ts new file mode 100644 index 0000000..c63a32d --- /dev/null +++ b/src/bot/events/interaction-create/execute/validators/command.ts @@ -0,0 +1,22 @@ +import type { Command } from '@commands/command' +import type { ChatInputCommandInteraction } from 'discord.js' +import { ExecuteCommandError } from '@events/interaction-create/execute/handlers/command' +import { DiscordAssert } from '@utils/discord' +import { PermissionsBitField } from 'discord.js' +import { ExecuteCommandMessage } from '../messages/command' + +export class ExecuteCommand extends ExecuteCommandMessage { + static override BASE_PERMS = [ + ...DiscordAssert.BASE_PERMS, + PermissionsBitField.Flags.UseApplicationCommands, + ] + + static getCommand(interaction: ChatInputCommandInteraction) { + const command: Command | undefined = interaction.client.commands.get(interaction.commandName) + if (!command) { + throw new ExecuteCommandError(this.ERR.NoMatchingCommand(interaction.commandName)) + } + + return command + } +} diff --git a/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts b/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts index b16356e..e9ff486 100644 --- a/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts +++ b/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts @@ -1,12 +1,10 @@ -import type { Event } from '@events/event' -import type { Interaction, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { ResetGrinderRoles } from '@events/client-ready/jobs/validators/reset-grinder-roles' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' export class ResetGrinderRolesButtonError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -16,17 +14,14 @@ export class ResetGrinderRolesButtonError extends DiscordBaseError { export const GOODBYE_NOTE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` -export default { - name: Events.InteractionCreate, +registerInteractionHandler({ desc: 'Opens goodbye note modal for users losing Grinder roles.', - async exec(_, interaction: Interaction) { + id: GOODBYE_NOTE_BUTTON_ID, + errorTag: () => `${GOODBYE_NOTE_BUTTON_ID}: ${ResetGrinderRoles.ERR.UnexpectedButton}`, + async exec(_, interaction) { if (!interaction.isButton()) return - const isValid = ResetGrinderRoles.assertComponentId(interaction.customId, GOODBYE_NOTE_BUTTON_ID) - if (!isValid) - return - try { if (!interaction.inCachedGuild()) throw new ResetGrinderRolesButtonError(ResetGrinderRoles.ERR.NotGuild) @@ -39,7 +34,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${GOODBYE_NOTE_BUTTON_ID}: ${ResetGrinderRoles.ERR.UnexpectedButton}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/message/handlers/send-modal.ts b/src/bot/events/interaction-create/message/handlers/send-modal.ts index 48400c1..4af351e 100644 --- a/src/bot/events/interaction-create/message/handlers/send-modal.ts +++ b/src/bot/events/interaction-create/message/handlers/send-modal.ts @@ -1,11 +1,9 @@ -import type { Event } from '@events/event' -import type { Attachment, Interaction } from 'discord.js' +import type { Attachment } from 'discord.js' import { EVENT_PATH } from '@events/index' +import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId, tempStore } from '@utils/component' import { getChannel, sendAsBot, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' import { Send } from '../validators/send' export class SendModalError extends DiscordBaseError { @@ -16,17 +14,14 @@ export class SendModalError extends DiscordBaseError { export const MESSAGE_SEND_ID = generateCustomId(EVENT_PATH, __filename) -export default { - name: Events.InteractionCreate, - desc: 'Handles modal submissions for creating an embed with a role-grant button.', - async exec(_, interaction: Interaction) { +registerInteractionHandler({ + desc: 'Handles message send modal submissions, posting messages (text/attachments) as the bot in the selected channel.', + id: MESSAGE_SEND_ID, + errorTag: () => `${MESSAGE_SEND_ID}: ${Send.ERR.UnexpectedModal}`, + async exec(_, interaction) { if (!interaction.isModalSubmit()) return - const isValidComponent = Send.assertComponentId(interaction.customId, MESSAGE_SEND_ID) - if (!isValidComponent) - return - try { if (!interaction.inCachedGuild()) throw new SendModalError(Send.ERR.NotGuild) @@ -51,7 +46,7 @@ export default { catch (err: any) { if (err instanceof DiscordBaseError) await sendReply(interaction, err.message) - else log.error(`Failed to handle ${MESSAGE_SEND_ID}: ${Send.ERR.UnexpectedModal}: ${err}`) + else throw err } }, -} as Event +}) diff --git a/src/bot/events/interaction-create/registry.ts b/src/bot/events/interaction-create/registry.ts new file mode 100644 index 0000000..41e27c0 --- /dev/null +++ b/src/bot/events/interaction-create/registry.ts @@ -0,0 +1,15 @@ +import type { Client, Interaction } from 'discord.js' + +export interface InteractionHandler { + desc: string + id: string + errorTag: () => string + match?: (interaction: Interaction) => boolean + exec: (client: Client, interaction: Interaction) => Promise | void +} + +export const interactionHandlers = new Map() + +export function registerInteractionHandler(handler: InteractionHandler) { + interactionHandlers.set(handler.id, handler) +} diff --git a/src/bot/events/message-create/im-fine/handlers/im-fine.ts b/src/bot/events/message-create/im-fine/handlers/index.ts similarity index 100% rename from src/bot/events/message-create/im-fine/handlers/im-fine.ts rename to src/bot/events/message-create/im-fine/handlers/index.ts diff --git a/src/bot/events/message-create/im-fine/messages/im-fine.ts b/src/bot/events/message-create/im-fine/messages/index.ts similarity index 100% rename from src/bot/events/message-create/im-fine/messages/im-fine.ts rename to src/bot/events/message-create/im-fine/messages/index.ts diff --git a/src/bot/events/message-create/im-fine/validators/im-fine.ts b/src/bot/events/message-create/im-fine/validators/im-fine.ts deleted file mode 100644 index 13d4941..0000000 --- a/src/bot/events/message-create/im-fine/validators/im-fine.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ImFineMessage } from '../messages/im-fine' - -export class ImFine extends ImFineMessage { -} diff --git a/src/bot/events/message-create/im-fine/validators/index.ts b/src/bot/events/message-create/im-fine/validators/index.ts new file mode 100644 index 0000000..66454d5 --- /dev/null +++ b/src/bot/events/message-create/im-fine/validators/index.ts @@ -0,0 +1,4 @@ +import { ImFineMessage } from '../messages' + +export class ImFine extends ImFineMessage { +} diff --git a/src/bot/events/message-reaction-add/checkin/handlers/submitted-checkin.ts b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts similarity index 99% rename from src/bot/events/message-reaction-add/checkin/handlers/submitted-checkin.ts rename to src/bot/events/message-reaction-add/checkin/handlers/submitted.ts index bbc422c..e3ed679 100644 --- a/src/bot/events/message-reaction-add/checkin/handlers/submitted-checkin.ts +++ b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts @@ -1,7 +1,7 @@ import type { Event } from '@events/event' import type { Client, MessageReaction, PartialMessageReaction, User } from 'discord.js' import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' -import { Checkin } from '@events/interaction-create/checkin/validators/checkin' +import { Checkin } from '@events/interaction-create/checkin/validators' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' import { Events } from 'discord.js' diff --git a/src/config/discord.ts b/src/config/discord.ts index 88ce3e9..8e031c1 100644 --- a/src/config/discord.ts +++ b/src/config/discord.ts @@ -13,8 +13,9 @@ export interface GrindRole { threshold: number } -export const GRINDER_ROLE = '1403320523756146768' +export const ARCHFYRE_ROLE = '1402625885684891658' export const FLAMEWARDEN_ROLE = '1403022712938561668' +export const GRINDER_ROLE = '1403320523756146768' const GRIND_ROLES: GrindRole[] = [ { diff --git a/src/utils/discord/assert.ts b/src/utils/discord/assert.ts index f1742ba..936d4b3 100644 --- a/src/utils/discord/assert.ts +++ b/src/utils/discord/assert.ts @@ -109,13 +109,6 @@ export class DiscordAssert extends DiscordMessage { } } - static assertComponentId(modalId: string, id: string): boolean { - if (!modalId.startsWith(id)) - return false - - return true - } - static isMemberHasRole(member: GuildMember, roleId: string): boolean { return member.roles.cache.has(roleId) } diff --git a/src/utils/io.ts b/src/utils/io.ts index a50a3db..62a0d26 100644 --- a/src/utils/io.ts +++ b/src/utils/io.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import { resolve } from 'node:path' -const EXCLUDED_FILES = new Set(['index.ts', 'messages.ts', 'validators.ts']) +const EXCLUDED_FILES = new Set(['registry.ts', 'messages.ts', 'validators.ts']) const EXCLUDED_FOLDERS = new Set(['validators', 'messages']) const EXCLUDED_PATTERNS: RegExp[] = [/\.d\.ts$/] From c3d1a170b5b2dd04ba634fa49202e6a6adf52cc5 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 20:15:18 +0700 Subject: [PATCH 06/18] refactor: `clientReady` and `messageCreate` events --- src/bot/events/client-ready/entry.ts | 21 +++++++++++++++ .../jobs/handlers/reset-grinder-roles.ts | 19 ++++++++------ src/bot/events/client-ready/registry.ts | 14 ++++++++++ .../client-ready/say-hello/handlers/index.ts | 23 +++++++--------- src/bot/events/interaction-create/entry.ts | 2 +- src/bot/events/interaction-create/registry.ts | 1 - .../channel/handlers/check-in.ts | 26 ++++++++++--------- src/bot/events/message-create/entry.ts | 22 ++++++++++++++++ .../message-create/im-fine/handlers/index.ts | 22 +++++++++------- src/bot/events/message-create/registry.ts | 15 +++++++++++ 10 files changed, 121 insertions(+), 44 deletions(-) create mode 100644 src/bot/events/client-ready/entry.ts create mode 100644 src/bot/events/client-ready/registry.ts create mode 100644 src/bot/events/message-create/entry.ts create mode 100644 src/bot/events/message-create/registry.ts diff --git a/src/bot/events/client-ready/entry.ts b/src/bot/events/client-ready/entry.ts new file mode 100644 index 0000000..8e22597 --- /dev/null +++ b/src/bot/events/client-ready/entry.ts @@ -0,0 +1,21 @@ +import type { Event } from '@events/event' +import type { Client } from 'discord.js' +import { log } from '@utils/logger' +import { Events } from 'discord.js' +import { clientReadyHandlers } from './registry' + +export default { + name: Events.ClientReady, + once: true, + desc: 'Runs all registered ClientReady handlers.', + async exec(client: Client) { + for (const handler of clientReadyHandlers) { + try { + await handler.exec(client) + } + catch (err) { + log.error(`ClientReady handler failed ${handler.errorTag()}: ${err}`) + } + } + }, +} as Event diff --git a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts index e8a6940..48a9186 100644 --- a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts @@ -1,11 +1,12 @@ -import type { Event } from '@events/event' import type { Client } from 'discord.js' import process from 'node:process' import { GRIND_ASHES_CHANNEL } from '@config/discord' +import { registerClientReadyHandler } from '@events/client-ready/registry' +import { EVENT_PATH } from '@events/index' +import { generateCustomId } from '@utils/component' import { getChannel } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' -import { Events } from 'discord.js' import cron from 'node-cron' import { ResetGrinderRoles } from '../validators/reset-grinder-roles' @@ -15,10 +16,12 @@ export class ResetGrinderRolesError extends DiscordBaseError { } } -export default { - name: Events.ClientReady, +export const RESET_GRINDER_ROLE_ID = generateCustomId(EVENT_PATH, __filename) + +registerClientReadyHandler({ + id: RESET_GRINDER_ROLE_ID, desc: `Reset Grinder roles for users that didn't do a check-in yesterday or the check-in didn't approved.`, - once: true, + errorTag: () => `${RESET_GRINDER_ROLE_ID}: ${ResetGrinderRoles.ERR.UnexpectedResetGrinderRoles}`, exec(client: Client) { try { cron.schedule('0 0 * * *', async () => { @@ -34,9 +37,9 @@ export default { log.success(ResetGrinderRoles.MSG.JobSuccess) }) } - catch (err: any) { + catch (err) { if (!(err instanceof DiscordBaseError)) - log.error(`Failed to handle ${ResetGrinderRoles.ERR.UnexpectedResetGrinderRoles}: ${err}`) + throw err } }, -} as Event +}) diff --git a/src/bot/events/client-ready/registry.ts b/src/bot/events/client-ready/registry.ts new file mode 100644 index 0000000..c91d882 --- /dev/null +++ b/src/bot/events/client-ready/registry.ts @@ -0,0 +1,14 @@ +import type { Client } from 'discord.js' + +export interface ClientReadyHandler { + desc: string + id: string + errorTag: () => string + exec: (client: Client) => Promise | void +} + +export const clientReadyHandlers: ClientReadyHandler[] = [] + +export function registerClientReadyHandler(handler: ClientReadyHandler) { + clientReadyHandlers.push(handler) +} diff --git a/src/bot/events/client-ready/say-hello/handlers/index.ts b/src/bot/events/client-ready/say-hello/handlers/index.ts index 90b6e74..a2d5ca0 100644 --- a/src/bot/events/client-ready/say-hello/handlers/index.ts +++ b/src/bot/events/client-ready/say-hello/handlers/index.ts @@ -1,8 +1,8 @@ -import type { Event } from '@events/event' import type { Client } from 'discord.js' +import { registerClientReadyHandler } from '@events/client-ready/registry' +import { EVENT_PATH } from '@events/index' +import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' import { SayHello } from '../validators' export class SayHelloError extends DiscordBaseError { @@ -11,16 +11,13 @@ export class SayHelloError extends DiscordBaseError { } } -export default { - name: Events.ClientReady, +export const SAY_HELLO_ID = generateCustomId(EVENT_PATH, __filename) + +registerClientReadyHandler({ + id: SAY_HELLO_ID, desc: 'Say こんにけは for the first load.', - once: true, + errorTag: () => `${SAY_HELLO_ID}: ${SayHello.ERR.UnexpectedSayHello}`, exec(client: Client) { - try { - console.warn(`こんにけは、${client.user?.tag}`) - } - catch (err: any) { - log.error(`Failed to handle ${SayHello.ERR.UnexpectedSayHello}: ${err}`) - } + console.warn(`こんにけは、${client.user?.tag}`) }, -} as Event +}) diff --git a/src/bot/events/interaction-create/entry.ts b/src/bot/events/interaction-create/entry.ts index 12b2530..886e9a8 100644 --- a/src/bot/events/interaction-create/entry.ts +++ b/src/bot/events/interaction-create/entry.ts @@ -21,7 +21,7 @@ export default { } catch (err) { await sendReply(interaction, `❓ Something weird happen... kindly contact <@&${ARCHFYRE_ROLE}> :)`) - log.error(`Interaction handler failed ${handler.errorTag()}: ${err}`) + log.error(`InteractionCreate handler failed ${handler.errorTag()}: ${err}`) } } } diff --git a/src/bot/events/interaction-create/registry.ts b/src/bot/events/interaction-create/registry.ts index 41e27c0..59585bf 100644 --- a/src/bot/events/interaction-create/registry.ts +++ b/src/bot/events/interaction-create/registry.ts @@ -4,7 +4,6 @@ export interface InteractionHandler { desc: string id: string errorTag: () => string - match?: (interaction: Interaction) => boolean exec: (client: Client, interaction: Interaction) => Promise | void } diff --git a/src/bot/events/message-create/channel/handlers/check-in.ts b/src/bot/events/message-create/channel/handlers/check-in.ts index 4126fda..f2be5a4 100644 --- a/src/bot/events/message-create/channel/handlers/check-in.ts +++ b/src/bot/events/message-create/channel/handlers/check-in.ts @@ -1,9 +1,11 @@ -import type { Event } from '@events/event' -import type { Message, TextChannel } from 'discord.js' +import type { TextChannel } from 'discord.js' import { CHECKIN_CHANNEL } from '@config/discord' +import { EVENT_PATH } from '@events/index' +import { registerMessageHandler } from '@events/message-create/registry' +import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' -import { ChannelType, Events } from 'discord.js' +import { ChannelType } from 'discord.js' import { CheckIn } from '../validators/check-in' export class CheckInError extends DiscordBaseError { @@ -12,10 +14,14 @@ export class CheckInError extends DiscordBaseError { } } -export default { - name: Events.MessageCreate, +export const CHECK_IN_CHANNEL_ID = generateCustomId(EVENT_PATH, __filename) + +registerMessageHandler({ + id: CHECK_IN_CHANNEL_ID, desc: 'Handle messages in channel for Check In event.', - async exec(_, msg: Message) { + errorTag: () => `${CHECK_IN_CHANNEL_ID}: ${CheckIn.ERR.UnexpectedCheckIn}`, + match: msg => msg.channel.id === CHECKIN_CHANNEL, + async exec(_, msg) { try { if (!msg.guild) throw new CheckInError(CheckIn.ERR.NotGuild) @@ -25,10 +31,6 @@ export default { if (channel.type !== ChannelType.GuildText) return - - if (channel.id !== CHECKIN_CHANNEL) - return - if (msg.author.bot) return @@ -37,7 +39,7 @@ export default { } catch (err: any) { if (!(err instanceof DiscordBaseError)) - log.error(`Failed to handle: ${CheckIn.ERR.UnexpectedCheckIn}: ${err}`) + throw err } }, -} as Event +}) diff --git a/src/bot/events/message-create/entry.ts b/src/bot/events/message-create/entry.ts new file mode 100644 index 0000000..606ff48 --- /dev/null +++ b/src/bot/events/message-create/entry.ts @@ -0,0 +1,22 @@ +import type { Event } from '@events/event' +import type { Message } from 'discord.js' +import { log } from '@utils/logger' +import { Events } from 'discord.js' +import { messageHandlers } from './registry' + +export default { + name: Events.MessageCreate, + desc: 'Dispatch all registered MessageCreate handlers.', + async exec(client, msg: Message) { + for (const handler of messageHandlers) { + try { + if (handler.match && !handler.match(msg)) + continue + await handler.exec(client, msg) + } + catch (err) { + log.error(`Message handler failed ${handler.errorTag()}: ${err}`) + } + } + }, +} as Event diff --git a/src/bot/events/message-create/im-fine/handlers/index.ts b/src/bot/events/message-create/im-fine/handlers/index.ts index 3c927a2..363c0c6 100644 --- a/src/bot/events/message-create/im-fine/handlers/index.ts +++ b/src/bot/events/message-create/im-fine/handlers/index.ts @@ -1,7 +1,8 @@ -import type { Event } from '@events/event' -import type { Message } from 'discord.js' +import { EVENT_PATH } from '@events/index' +import { registerMessageHandler } from '@events/message-create/registry' +import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' -import { Events } from 'discord.js' +import { ImFine } from '../validators' export class ImFineError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -9,11 +10,14 @@ export class ImFineError extends DiscordBaseError { } } -export default { - name: Events.MessageCreate, +export const IM_FINE_ID = generateCustomId(EVENT_PATH, __filename) + +registerMessageHandler({ desc: 'Replying to a user when the user\'s chat contains \'fine\' word.', - async exec(_, msg: Message) { - if (!msg.author.bot && msg.content.includes('fine')) - await msg.reply('gua i\'m fineπŸ˜…') + id: IM_FINE_ID, + errorTag: () => `${IM_FINE_ID}: ${ImFine.ERR.UnexpectedImFine}`, + match: msg => !msg.author.bot && msg.content.includes('fine'), + async exec(_, msg) { + await msg.reply('gua I\'m fineπŸ˜…') }, -} as Event +}) diff --git a/src/bot/events/message-create/registry.ts b/src/bot/events/message-create/registry.ts new file mode 100644 index 0000000..73206ef --- /dev/null +++ b/src/bot/events/message-create/registry.ts @@ -0,0 +1,15 @@ +import type { Client, Message } from 'discord.js' + +export interface MessageHandler { + desc: string + id: string + errorTag: () => string + match?: (msg: Message) => boolean + exec: (client: Client, msg: Message) => Promise | void +} + +export const messageHandlers: MessageHandler[] = [] + +export function registerMessageHandler(handler: MessageHandler) { + messageHandlers.push(handler) +} From 3447add6b9263873b83918d4b634d05f696bcfb2 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 20:25:25 +0700 Subject: [PATCH 07/18] refactor: `guild-member-update` and `message-reaction-add` events --- src/bot/events/guild-member-update/entry.ts | 22 ++++++++++++++++ .../grinder-role/handlers/index.ts | 21 ++++++++------- .../events/guild-member-update/registry.ts | 15 +++++++++++ .../checkin/handlers/submitted.ts | 26 ++++++++++--------- src/bot/events/message-reaction-add/entry.ts | 22 ++++++++++++++++ .../events/message-reaction-add/registry.ts | 15 +++++++++++ 6 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 src/bot/events/guild-member-update/entry.ts create mode 100644 src/bot/events/guild-member-update/registry.ts create mode 100644 src/bot/events/message-reaction-add/entry.ts create mode 100644 src/bot/events/message-reaction-add/registry.ts diff --git a/src/bot/events/guild-member-update/entry.ts b/src/bot/events/guild-member-update/entry.ts new file mode 100644 index 0000000..feed010 --- /dev/null +++ b/src/bot/events/guild-member-update/entry.ts @@ -0,0 +1,22 @@ +import type { Event } from '@events/event' +import type { GuildMember } from 'discord.js' +import { log } from '@utils/logger' +import { Events } from 'discord.js' +import { guildMemberUpdateHandlers } from './registry' + +export default { + name: Events.GuildMemberUpdate, + desc: 'Dispatch all registered GuildMemberUpdate handlers.', + async exec(client, oldMember: GuildMember, newMember: GuildMember) { + for (const handler of guildMemberUpdateHandlers) { + try { + if (handler.match && !handler.match(oldMember, newMember)) + continue + await handler.exec(client, oldMember, newMember) + } + catch (err) { + log.error(`GuildMemberUpdate handler failed ${handler.errorTag()}: ${err}`) + } + } + }, +} as Event diff --git a/src/bot/events/guild-member-update/grinder-role/handlers/index.ts b/src/bot/events/guild-member-update/grinder-role/handlers/index.ts index fc8bea1..5ace05c 100644 --- a/src/bot/events/guild-member-update/grinder-role/handlers/index.ts +++ b/src/bot/events/guild-member-update/grinder-role/handlers/index.ts @@ -1,10 +1,9 @@ -import type { Event } from '@events/event' -import type { GuildMember } from 'discord.js' import { GRIND_ASHES_CHANNEL, GRINDER_ROLE } from '@config/discord' +import { registerGuildMemberUpdateHandler } from '@events/guild-member-update/registry' +import { EVENT_PATH } from '@events/index' +import { generateCustomId } from '@utils/component' import { getChannel, sendAsBot } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' import { GrinderRole } from '../validators' export class GrinderRoleError extends DiscordBaseError { @@ -13,10 +12,14 @@ export class GrinderRoleError extends DiscordBaseError { } } -export default { - name: Events.GuildMemberUpdate, +export const GRINDER_ROLE_ID = generateCustomId(EVENT_PATH, __filename) + +registerGuildMemberUpdateHandler({ + id: GRINDER_ROLE_ID, desc: 'Watches grinder role assignment/removal for members on guild member update.', - async exec(_, oldMember: GuildMember, newMember: GuildMember) { + errorTag: () => `${GRINDER_ROLE_ID}: ${GrinderRole.ERR.UnexpectedGrinderRole}`, + match: (_, newMember) => GrinderRole.isMemberHasRole(newMember, GRINDER_ROLE), + async exec(_, oldMember, newMember) { try { if (!newMember.guild) throw new GrinderRoleError(GrinderRole.ERR.NotGuild) @@ -36,7 +39,7 @@ export default { } catch (err: any) { if (!(err instanceof DiscordBaseError)) - log.error(`Failed to handle: ${GrinderRole.ERR.UnexpectedGrinderRole}: ${err}`) + throw err } }, -} as Event +}) diff --git a/src/bot/events/guild-member-update/registry.ts b/src/bot/events/guild-member-update/registry.ts new file mode 100644 index 0000000..1bd94af --- /dev/null +++ b/src/bot/events/guild-member-update/registry.ts @@ -0,0 +1,15 @@ +import type { Client, GuildMember } from 'discord.js' + +export interface GuildMemberUpdateHandler { + id: string + desc: string + errorTag: () => string + match?: (oldMember: GuildMember, newMember: GuildMember) => boolean + exec: (client: Client, oldMember: GuildMember, newMember: GuildMember) => Promise | void +} + +export const guildMemberUpdateHandlers: GuildMemberUpdateHandler[] = [] + +export function registerGuildMemberUpdateHandler(handler: GuildMemberUpdateHandler) { + guildMemberUpdateHandlers.push(handler) +} diff --git a/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts index e3ed679..d884be5 100644 --- a/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts +++ b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts @@ -1,10 +1,9 @@ -import type { Event } from '@events/event' -import type { Client, MessageReaction, PartialMessageReaction, User } from 'discord.js' import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' +import { EVENT_PATH } from '@events/index' import { Checkin } from '@events/interaction-create/checkin/validators' +import { registerReactionHandler } from '@events/message-reaction-add/registry' +import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' -import { log } from '@utils/logger' -import { Events } from 'discord.js' export class SubmittedCheckinError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -12,16 +11,19 @@ export class SubmittedCheckinError extends DiscordBaseError { } } -export default { - name: Events.MessageReactionAdd, +export const SUBMITTED_CHECKIN_ID = generateCustomId(EVENT_PATH, __filename) + +registerReactionHandler({ + id: SUBMITTED_CHECKIN_ID, desc: 'Handles user-submitted checkin submissions with reacted by Flamewarden whether approved or rejected.', - async exec(client: Client, reaction: MessageReaction | PartialMessageReaction, user: User) { + errorTag: () => `${SUBMITTED_CHECKIN_ID}: ${Checkin.ERR.UnexpectedSubmittedCheckinMessage}`, + match: (_, user) => !user.bot, + async exec(client, reaction, user) { const message = reaction.message const guild = message.guild - if (user.bot) - return - if (!message.inGuild() || !guild) + if (!guild || !message.inGuild()) return + if (reaction.partial) await reaction.fetch() if (message.partial) @@ -45,7 +47,7 @@ export default { } catch (err: any) { if (!(err instanceof DiscordBaseError)) - log.error(`Failed to handle: ${Checkin.ERR.UnexpectedSubmittedCheckinMessage}: ${err}`) + throw err } }, -} as Event +}) diff --git a/src/bot/events/message-reaction-add/entry.ts b/src/bot/events/message-reaction-add/entry.ts new file mode 100644 index 0000000..5278896 --- /dev/null +++ b/src/bot/events/message-reaction-add/entry.ts @@ -0,0 +1,22 @@ +import type { Event } from '@events/event' +import type { MessageReaction, User } from 'discord.js' +import { log } from '@utils/logger' +import { Events } from 'discord.js' +import { reactionHandlers } from './registry' + +export default { + name: Events.MessageReactionAdd, + desc: 'Dispatch all registered MessageReactionAdd handlers.', + async exec(client, reaction: MessageReaction, user: User) { + for (const handler of reactionHandlers) { + try { + if (handler.match && !handler.match(reaction, user)) + continue + await handler.exec(client, reaction, user) + } + catch (err) { + log.error(`Reaction handler failed ${handler.errorTag()}: ${err}`) + } + } + }, +} as Event diff --git a/src/bot/events/message-reaction-add/registry.ts b/src/bot/events/message-reaction-add/registry.ts new file mode 100644 index 0000000..aa8680b --- /dev/null +++ b/src/bot/events/message-reaction-add/registry.ts @@ -0,0 +1,15 @@ +import type { Client, MessageReaction, User } from 'discord.js' + +export interface ReactionHandler { + id: string + desc: string + errorTag: () => string + match?: (reaction: MessageReaction, user: User) => boolean + exec: (client: Client, reaction: MessageReaction, user: User) => Promise | void +} + +export const reactionHandlers: ReactionHandler[] = [] + +export function registerReactionHandler(handler: ReactionHandler) { + reactionHandlers.push(handler) +} From 42f8ed9884e0a53a6feb98c1c8b5e96ecb0c8ba5 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 20:40:43 +0700 Subject: [PATCH 08/18] refactor: event and command index --- src/bot/commands/command.d.ts | 2 +- src/bot/commands/index.ts | 10 +++++----- src/bot/events/index.ts | 4 +++- src/deploy-commands.ts | 7 +++++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/bot/commands/command.d.ts b/src/bot/commands/command.d.ts index 8421c82..4d80767 100644 --- a/src/bot/commands/command.d.ts +++ b/src/bot/commands/command.d.ts @@ -1,4 +1,4 @@ -import type { ChatInputCommandInteraction, Client } from 'discord.js' +import type { ChatInputCommandInteraction, Client, SlashCommandBuilder } from 'discord.js' export interface Command { data: SlashCommandBuilder diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index 06cf1c8..b0d5f4a 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -20,14 +20,14 @@ export async function registerCommands(client: Client) { client.commands = new Collection() for (const file of files) { - const { default: command } = await import(file) as { default: Command } const fileName = getModuleName(COMMAND_PATH, file) + const { default: command } = await import(file) as { default: Command } + if (!command) + continue try { - if (command) { - log.info(`Registering command ${fileName}...`) - client.commands.set(command.data.name, command) - } + log.info(`Registering command ${fileName}...`) + client.commands.set(command.data.name, command) } catch (err: any) { const msg = err instanceof CommandError ? err.message : '❌ Something went wrong when importing the command' diff --git a/src/bot/events/index.ts b/src/bot/events/index.ts index b1de852..bfa6b03 100644 --- a/src/bot/events/index.ts +++ b/src/bot/events/index.ts @@ -17,8 +17,10 @@ const files = readFiles(__dirname) export async function registerEvents(client: Client) { for (const file of files) { - const { default: event } = await import(file) as { default: Event } const fileName = getModuleName(EVENT_PATH, file) + const { default: event } = await import(file) as { default: Event } + if (!event) + continue try { if (event) { diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index ebfeb6d..bde815d 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -16,9 +16,12 @@ async function loadCommands(): Promise = await Promise.all( files.map(async (file) => { const fileName = getModuleName(root, file) - log.info(`Registering command ${fileName}...`) + const { default: command } = await import(file) as { default: Command } + if (!command) + return null + try { - const { default: command } = (await import(file)) as { default: Command } + log.info(`Deploying command ${fileName}...`) if ('data' in command && 'execute' in command) { return command.data.toJSON() } From a397810a014aabba2b3652ffd9666b69b0c48ed2 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 21:37:06 +0700 Subject: [PATCH 09/18] fix: fallback for non custom id --- src/bot/events/interaction-create/entry.ts | 12 ++++++++++-- src/bot/events/interaction-create/registry.ts | 11 ++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/bot/events/interaction-create/entry.ts b/src/bot/events/interaction-create/entry.ts index 886e9a8..fa4848c 100644 --- a/src/bot/events/interaction-create/entry.ts +++ b/src/bot/events/interaction-create/entry.ts @@ -5,7 +5,7 @@ import { decodeSnowflakes } from '@utils/component' import { sendReply } from '@utils/discord' import { log } from '@utils/logger' import { Events } from 'discord.js' -import { interactionHandlers } from './registry' +import { interactionHandlerMap, interactionHandlers } from './registry' export default { name: Events.InteractionCreate, @@ -14,10 +14,11 @@ export default { if ('customId' in interaction && interaction.customId) { const [prefix] = decodeSnowflakes(interaction.customId) - const handler = interactionHandlers.get(prefix) + const handler = interactionHandlerMap.get(prefix) if (handler) { try { await handler.exec(client, interaction) + return } catch (err) { await sendReply(interaction, `❓ Something weird happen... kindly contact <@&${ARCHFYRE_ROLE}> :)`) @@ -25,5 +26,12 @@ export default { } } } + + for (const handler of interactionHandlers) { + if (handler.match && !handler.match(interaction)) + continue + + await handler.exec(client, interaction) + } }, } as Event diff --git a/src/bot/events/interaction-create/registry.ts b/src/bot/events/interaction-create/registry.ts index 59585bf..8f35d44 100644 --- a/src/bot/events/interaction-create/registry.ts +++ b/src/bot/events/interaction-create/registry.ts @@ -2,13 +2,18 @@ import type { Client, Interaction } from 'discord.js' export interface InteractionHandler { desc: string - id: string + id?: string errorTag: () => string + match?: (interaction: Interaction) => boolean exec: (client: Client, interaction: Interaction) => Promise | void } -export const interactionHandlers = new Map() +export const interactionHandlerMap = new Map() +export const interactionHandlers: InteractionHandler[] = [] export function registerInteractionHandler(handler: InteractionHandler) { - interactionHandlers.set(handler.id, handler) + if (handler.id) + interactionHandlerMap.set(handler.id, handler) + else + interactionHandlers.push(handler) } From aabd2dbe02f4a113604f3e20715bdd836b38dcf4 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 21:37:22 +0700 Subject: [PATCH 10/18] fix: do not include execute command into non custom id --- .../events/interaction-create/execute/handlers/command.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/bot/events/interaction-create/execute/handlers/command.ts b/src/bot/events/interaction-create/execute/handlers/command.ts index 9052453..5a362ea 100644 --- a/src/bot/events/interaction-create/execute/handlers/command.ts +++ b/src/bot/events/interaction-create/execute/handlers/command.ts @@ -1,8 +1,6 @@ import type { Command } from '@commands/command' -import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' -import { EVENT_PATH } from '../../..' import { registerInteractionHandler } from '../../registry' import { ExecuteCommand } from '../validators/command' @@ -12,12 +10,9 @@ export class ExecuteCommandError extends DiscordBaseError { } } -export const EXECUTE_COMMAND_ID = `${generateCustomId(EVENT_PATH, __filename)}` - registerInteractionHandler({ desc: 'Executing a command when an interaction is created.', - id: EXECUTE_COMMAND_ID, - errorTag: () => `${EXECUTE_COMMAND_ID}`, + errorTag: () => `execute-command`, async exec(client, interaction) { if (!interaction.isChatInputCommand()) return From 0604e118323fe866b23e53b62efa37abafac5d29 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 21:49:14 +0700 Subject: [PATCH 11/18] refactor: unused ids into module name --- .../client-ready/jobs/handlers/reset-grinder-roles.ts | 7 +++---- src/bot/events/client-ready/registry.ts | 1 - src/bot/events/client-ready/say-hello/handlers/index.ts | 7 +++---- .../guild-member-update/grinder-role/handlers/index.ts | 7 +++---- src/bot/events/guild-member-update/registry.ts | 1 - .../events/interaction-create/execute/handlers/command.ts | 6 +++++- src/bot/events/message-create/channel/handlers/check-in.ts | 7 +++---- src/bot/events/message-create/im-fine/handlers/index.ts | 7 +++---- src/bot/events/message-create/registry.ts | 1 - .../message-reaction-add/checkin/handlers/submitted.ts | 7 +++---- src/bot/events/message-reaction-add/registry.ts | 1 - 11 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts index 48a9186..2e14aa4 100644 --- a/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts +++ b/src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts @@ -3,9 +3,9 @@ import process from 'node:process' import { GRIND_ASHES_CHANNEL } from '@config/discord' import { registerClientReadyHandler } from '@events/client-ready/registry' import { EVENT_PATH } from '@events/index' -import { generateCustomId } from '@utils/component' import { getChannel } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { log } from '@utils/logger' import cron from 'node-cron' import { ResetGrinderRoles } from '../validators/reset-grinder-roles' @@ -16,12 +16,11 @@ export class ResetGrinderRolesError extends DiscordBaseError { } } -export const RESET_GRINDER_ROLE_ID = generateCustomId(EVENT_PATH, __filename) +const moduleName = getModuleName(EVENT_PATH, __filename) registerClientReadyHandler({ - id: RESET_GRINDER_ROLE_ID, desc: `Reset Grinder roles for users that didn't do a check-in yesterday or the check-in didn't approved.`, - errorTag: () => `${RESET_GRINDER_ROLE_ID}: ${ResetGrinderRoles.ERR.UnexpectedResetGrinderRoles}`, + errorTag: () => `${moduleName}: ${ResetGrinderRoles.ERR.UnexpectedResetGrinderRoles}`, exec(client: Client) { try { cron.schedule('0 0 * * *', async () => { diff --git a/src/bot/events/client-ready/registry.ts b/src/bot/events/client-ready/registry.ts index c91d882..0a95e65 100644 --- a/src/bot/events/client-ready/registry.ts +++ b/src/bot/events/client-ready/registry.ts @@ -2,7 +2,6 @@ import type { Client } from 'discord.js' export interface ClientReadyHandler { desc: string - id: string errorTag: () => string exec: (client: Client) => Promise | void } diff --git a/src/bot/events/client-ready/say-hello/handlers/index.ts b/src/bot/events/client-ready/say-hello/handlers/index.ts index a2d5ca0..48a638e 100644 --- a/src/bot/events/client-ready/say-hello/handlers/index.ts +++ b/src/bot/events/client-ready/say-hello/handlers/index.ts @@ -1,8 +1,8 @@ import type { Client } from 'discord.js' import { registerClientReadyHandler } from '@events/client-ready/registry' import { EVENT_PATH } from '@events/index' -import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { SayHello } from '../validators' export class SayHelloError extends DiscordBaseError { @@ -11,12 +11,11 @@ export class SayHelloError extends DiscordBaseError { } } -export const SAY_HELLO_ID = generateCustomId(EVENT_PATH, __filename) +const moduleName = getModuleName(EVENT_PATH, __filename) registerClientReadyHandler({ - id: SAY_HELLO_ID, desc: 'Say こんにけは for the first load.', - errorTag: () => `${SAY_HELLO_ID}: ${SayHello.ERR.UnexpectedSayHello}`, + errorTag: () => `${moduleName}: ${SayHello.ERR.UnexpectedSayHello}`, exec(client: Client) { console.warn(`こんにけは、${client.user?.tag}`) }, diff --git a/src/bot/events/guild-member-update/grinder-role/handlers/index.ts b/src/bot/events/guild-member-update/grinder-role/handlers/index.ts index 5ace05c..888d012 100644 --- a/src/bot/events/guild-member-update/grinder-role/handlers/index.ts +++ b/src/bot/events/guild-member-update/grinder-role/handlers/index.ts @@ -1,9 +1,9 @@ import { GRIND_ASHES_CHANNEL, GRINDER_ROLE } from '@config/discord' import { registerGuildMemberUpdateHandler } from '@events/guild-member-update/registry' import { EVENT_PATH } from '@events/index' -import { generateCustomId } from '@utils/component' import { getChannel, sendAsBot } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { GrinderRole } from '../validators' export class GrinderRoleError extends DiscordBaseError { @@ -12,12 +12,11 @@ export class GrinderRoleError extends DiscordBaseError { } } -export const GRINDER_ROLE_ID = generateCustomId(EVENT_PATH, __filename) +const moduleName = getModuleName(EVENT_PATH, __filename) registerGuildMemberUpdateHandler({ - id: GRINDER_ROLE_ID, desc: 'Watches grinder role assignment/removal for members on guild member update.', - errorTag: () => `${GRINDER_ROLE_ID}: ${GrinderRole.ERR.UnexpectedGrinderRole}`, + errorTag: () => `${moduleName}: ${GrinderRole.ERR.UnexpectedGrinderRole}`, match: (_, newMember) => GrinderRole.isMemberHasRole(newMember, GRINDER_ROLE), async exec(_, oldMember, newMember) { try { diff --git a/src/bot/events/guild-member-update/registry.ts b/src/bot/events/guild-member-update/registry.ts index 1bd94af..076609f 100644 --- a/src/bot/events/guild-member-update/registry.ts +++ b/src/bot/events/guild-member-update/registry.ts @@ -1,7 +1,6 @@ import type { Client, GuildMember } from 'discord.js' export interface GuildMemberUpdateHandler { - id: string desc: string errorTag: () => string match?: (oldMember: GuildMember, newMember: GuildMember) => boolean diff --git a/src/bot/events/interaction-create/execute/handlers/command.ts b/src/bot/events/interaction-create/execute/handlers/command.ts index 5a362ea..20eea85 100644 --- a/src/bot/events/interaction-create/execute/handlers/command.ts +++ b/src/bot/events/interaction-create/execute/handlers/command.ts @@ -1,6 +1,8 @@ import type { Command } from '@commands/command' +import { EVENT_PATH } from '@events/index' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { registerInteractionHandler } from '../../registry' import { ExecuteCommand } from '../validators/command' @@ -10,9 +12,11 @@ export class ExecuteCommandError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) + registerInteractionHandler({ desc: 'Executing a command when an interaction is created.', - errorTag: () => `execute-command`, + errorTag: () => `${moduleName}: ${ExecuteCommand.ERR.UnexpectedExecuteCommand}`, async exec(client, interaction) { if (!interaction.isChatInputCommand()) return diff --git a/src/bot/events/message-create/channel/handlers/check-in.ts b/src/bot/events/message-create/channel/handlers/check-in.ts index f2be5a4..b2330ae 100644 --- a/src/bot/events/message-create/channel/handlers/check-in.ts +++ b/src/bot/events/message-create/channel/handlers/check-in.ts @@ -2,8 +2,8 @@ import type { TextChannel } from 'discord.js' import { CHECKIN_CHANNEL } from '@config/discord' import { EVENT_PATH } from '@events/index' import { registerMessageHandler } from '@events/message-create/registry' -import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { log } from '@utils/logger' import { ChannelType } from 'discord.js' import { CheckIn } from '../validators/check-in' @@ -14,12 +14,11 @@ export class CheckInError extends DiscordBaseError { } } -export const CHECK_IN_CHANNEL_ID = generateCustomId(EVENT_PATH, __filename) +const moduleName = getModuleName(EVENT_PATH, __filename) registerMessageHandler({ - id: CHECK_IN_CHANNEL_ID, desc: 'Handle messages in channel for Check In event.', - errorTag: () => `${CHECK_IN_CHANNEL_ID}: ${CheckIn.ERR.UnexpectedCheckIn}`, + errorTag: () => `${moduleName}: ${CheckIn.ERR.UnexpectedCheckIn}`, match: msg => msg.channel.id === CHECKIN_CHANNEL, async exec(_, msg) { try { diff --git a/src/bot/events/message-create/im-fine/handlers/index.ts b/src/bot/events/message-create/im-fine/handlers/index.ts index 363c0c6..54f21d9 100644 --- a/src/bot/events/message-create/im-fine/handlers/index.ts +++ b/src/bot/events/message-create/im-fine/handlers/index.ts @@ -1,7 +1,7 @@ import { EVENT_PATH } from '@events/index' import { registerMessageHandler } from '@events/message-create/registry' -import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { ImFine } from '../validators' export class ImFineError extends DiscordBaseError { @@ -10,12 +10,11 @@ export class ImFineError extends DiscordBaseError { } } -export const IM_FINE_ID = generateCustomId(EVENT_PATH, __filename) +const moduleName = getModuleName(EVENT_PATH, __filename) registerMessageHandler({ desc: 'Replying to a user when the user\'s chat contains \'fine\' word.', - id: IM_FINE_ID, - errorTag: () => `${IM_FINE_ID}: ${ImFine.ERR.UnexpectedImFine}`, + errorTag: () => `${moduleName}: ${ImFine.ERR.UnexpectedImFine}`, match: msg => !msg.author.bot && msg.content.includes('fine'), async exec(_, msg) { await msg.reply('gua I\'m fineπŸ˜…') diff --git a/src/bot/events/message-create/registry.ts b/src/bot/events/message-create/registry.ts index 73206ef..5672005 100644 --- a/src/bot/events/message-create/registry.ts +++ b/src/bot/events/message-create/registry.ts @@ -2,7 +2,6 @@ import type { Client, Message } from 'discord.js' export interface MessageHandler { desc: string - id: string errorTag: () => string match?: (msg: Message) => boolean exec: (client: Client, msg: Message) => Promise | void diff --git a/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts index d884be5..fdd185d 100644 --- a/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts +++ b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts @@ -2,8 +2,8 @@ import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' import { Checkin } from '@events/interaction-create/checkin/validators' import { registerReactionHandler } from '@events/message-reaction-add/registry' -import { generateCustomId } from '@utils/component' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' export class SubmittedCheckinError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -11,12 +11,11 @@ export class SubmittedCheckinError extends DiscordBaseError { } } -export const SUBMITTED_CHECKIN_ID = generateCustomId(EVENT_PATH, __filename) +const moduleName = getModuleName(EVENT_PATH, __filename) registerReactionHandler({ - id: SUBMITTED_CHECKIN_ID, desc: 'Handles user-submitted checkin submissions with reacted by Flamewarden whether approved or rejected.', - errorTag: () => `${SUBMITTED_CHECKIN_ID}: ${Checkin.ERR.UnexpectedSubmittedCheckinMessage}`, + errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedSubmittedCheckinMessage}`, match: (_, user) => !user.bot, async exec(client, reaction, user) { const message = reaction.message diff --git a/src/bot/events/message-reaction-add/registry.ts b/src/bot/events/message-reaction-add/registry.ts index aa8680b..c112b95 100644 --- a/src/bot/events/message-reaction-add/registry.ts +++ b/src/bot/events/message-reaction-add/registry.ts @@ -1,7 +1,6 @@ import type { Client, MessageReaction, User } from 'discord.js' export interface ReactionHandler { - id: string desc: string errorTag: () => string match?: (reaction: MessageReaction, user: User) => boolean From f751982845505135a0589803ac5e1df43570aff8 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 21:54:14 +0700 Subject: [PATCH 12/18] refactor: component's error tag using module name --- .../interaction-create/checkin/handlers/approve-button.ts | 4 +++- .../events/interaction-create/checkin/handlers/audit-modal.ts | 4 +++- .../checkin/handlers/custom-button-modal.ts | 4 +++- .../interaction-create/checkin/handlers/custom-button.ts | 4 +++- src/bot/events/interaction-create/checkin/handlers/modal.ts | 4 +++- .../interaction-create/checkin/handlers/reject-button.ts | 4 +++- .../embed/handlers/role-grant-create-button.ts | 4 +++- .../embed/handlers/role-grant-create-modal.ts | 4 +++- .../jobs/handlers/reset-grinder-roles-button.ts | 4 +++- .../events/interaction-create/message/handlers/send-modal.ts | 4 +++- 10 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/bot/events/interaction-create/checkin/handlers/approve-button.ts b/src/bot/events/interaction-create/checkin/handlers/approve-button.ts index 3a7522e..7664cec 100644 --- a/src/bot/events/interaction-create/checkin/handlers/approve-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/approve-button.ts @@ -5,6 +5,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Checkin } from '../validators' export class CheckinApproveButtonError extends DiscordBaseError { @@ -13,12 +14,13 @@ export class CheckinApproveButtonError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const CHECKIN_APPROVE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Approves a user check-in from the approve button.', id: CHECKIN_APPROVE_BUTTON_ID, - errorTag: () => `${CHECKIN_APPROVE_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}`, + errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedButton}`, async exec(client, interaction) { if (!interaction.isButton()) return diff --git a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts index 80ccaee..57ae212 100644 --- a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -6,6 +6,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Checkin } from '../validators' import { CheckinAudit } from '../validators/audit' @@ -15,12 +16,13 @@ export class CheckinAuditModalError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const CHECKIN_AUDIT_ID = generateCustomId(EVENT_PATH, __filename) registerInteractionHandler({ desc: 'Handles modal submissions for check-in audit modal forms.', id: CHECKIN_AUDIT_ID, - errorTag: () => `${CHECKIN_AUDIT_ID}: ${CheckinAudit.ERR.UnexpectedModal}`, + errorTag: () => `${moduleName}: ${CheckinAudit.ERR.UnexpectedModal}`, async exec(client, interaction) { if (!interaction.isModalSubmit()) return diff --git a/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts b/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts index 5dba028..f696b31 100644 --- a/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts @@ -6,6 +6,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Checkin } from '../validators' export class CheckinCustomButtonModalError extends DiscordBaseError { @@ -14,12 +15,13 @@ export class CheckinCustomButtonModalError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const CHECKIN_CUSTOM_BUTTON_MODAL_ID = generateCustomId(EVENT_PATH, __filename) registerInteractionHandler({ desc: 'Handles modal submissions for the custom check-in review modal.', id: CHECKIN_CUSTOM_BUTTON_MODAL_ID, - errorTag: () => `${CHECKIN_CUSTOM_BUTTON_MODAL_ID}: ${Checkin.ERR.UnexpectedModal}`, + errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedModal}`, async exec(client, interaction) { if (!interaction.isModalSubmit()) return diff --git a/src/bot/events/interaction-create/checkin/handlers/custom-button.ts b/src/bot/events/interaction-create/checkin/handlers/custom-button.ts index 636ab94..7fa3753 100644 --- a/src/bot/events/interaction-create/checkin/handlers/custom-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button.ts @@ -5,6 +5,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { createCheckinReviewModal, encodeSnowflake, generateCustomId, getCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Checkin } from '../validators' import { CHECKIN_CUSTOM_BUTTON_MODAL_ID } from './custom-button-modal' @@ -14,12 +15,13 @@ export class CheckinCustomButtonError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const CHECKIN_CUSTOM_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Opens review modal for a check-in', id: CHECKIN_CUSTOM_BUTTON_ID, - errorTag: () => `${CHECKIN_CUSTOM_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}`, + errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedButton}`, async exec(client, interaction) { if (!interaction.isButton()) return diff --git a/src/bot/events/interaction-create/checkin/handlers/modal.ts b/src/bot/events/interaction-create/checkin/handlers/modal.ts index 1ca0a8a..9be947f 100644 --- a/src/bot/events/interaction-create/checkin/handlers/modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/modal.ts @@ -5,6 +5,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId, tempStore } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Checkin } from '../validators' export class CheckinModalError extends DiscordBaseError { @@ -13,12 +14,13 @@ export class CheckinModalError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const CHECKIN_ID = generateCustomId(EVENT_PATH, __filename) registerInteractionHandler({ desc: 'Handles modal submissions for check-in modal forms.', id: CHECKIN_ID, - errorTag: () => `${CHECKIN_ID}: ${Checkin.ERR.UnexpectedModal}`, + errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedModal}`, async exec(client, interaction) { if (!interaction.isModalSubmit()) return diff --git a/src/bot/events/interaction-create/checkin/handlers/reject-button.ts b/src/bot/events/interaction-create/checkin/handlers/reject-button.ts index 10884e6..ae80cea 100644 --- a/src/bot/events/interaction-create/checkin/handlers/reject-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/reject-button.ts @@ -5,6 +5,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Checkin } from '../validators' export class CheckinRejectButtonError extends DiscordBaseError { @@ -13,12 +14,13 @@ export class CheckinRejectButtonError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const CHECKIN_REJECT_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles check-in reject button interactions and rejects user check-in.', id: CHECKIN_REJECT_BUTTON_ID, - errorTag: () => `${CHECKIN_REJECT_BUTTON_ID}: ${Checkin.ERR.UnexpectedButton}`, + errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedButton}`, async exec(client, interaction) { if (!interaction.isButton()) return diff --git a/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts b/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts index 269e762..0ec6287 100644 --- a/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts +++ b/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts @@ -4,6 +4,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { getRole, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { RoleGrantCreate } from '../validators/role-grant-create' export class EmbedRoleGrantButtonError extends DiscordBaseError { @@ -12,12 +13,13 @@ export class EmbedRoleGrantButtonError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const EMBED_ROLE_GRANT_CREATE_BUTTON_ID = generateCustomId(EVENT_PATH, __filename) registerInteractionHandler({ desc: 'Handles role assignment button interactions and adds a role for users.', id: EMBED_ROLE_GRANT_CREATE_BUTTON_ID, - errorTag: () => `${EMBED_ROLE_GRANT_CREATE_BUTTON_ID}: ${RoleGrantCreate.ERR.UnexpectedButton}`, + errorTag: () => `${moduleName}: ${RoleGrantCreate.ERR.UnexpectedButton}`, async exec(_, interaction) { if (!interaction.isButton()) return diff --git a/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts b/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts index 94ffed5..64cf0bf 100644 --- a/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts +++ b/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts @@ -3,6 +3,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { createEmbed, encodeSnowflake, generateCustomId, getCustomId } from '@utils/component' import { getChannel, getRole, sendAsBot, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js' import { RoleGrantCreate } from '../validators/role-grant-create' import { EMBED_ROLE_GRANT_CREATE_BUTTON_ID } from './role-grant-create-button' @@ -13,12 +14,13 @@ export class EmbedRoleGrantModalError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const EMBED_ROLE_GRANT_CREATE_MODAL_ID = generateCustomId(EVENT_PATH, __filename) registerInteractionHandler({ desc: 'Handles modal submissions for creating an embed with a role-grant button.', id: EMBED_ROLE_GRANT_CREATE_MODAL_ID, - errorTag: () => `${EMBED_ROLE_GRANT_CREATE_MODAL_ID}: ${RoleGrantCreate.ERR.UnexpectedModal}`, + errorTag: () => `${moduleName}: ${RoleGrantCreate.ERR.UnexpectedModal}`, async exec(_, interaction) { if (!interaction.isModalSubmit()) return diff --git a/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts b/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts index e9ff486..5bfa45c 100644 --- a/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts +++ b/src/bot/events/interaction-create/jobs/handlers/reset-grinder-roles-button.ts @@ -5,6 +5,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' export class ResetGrinderRolesButtonError extends DiscordBaseError { constructor(message: string, options?: { cause?: unknown }) { @@ -12,12 +13,13 @@ export class ResetGrinderRolesButtonError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const GOODBYE_NOTE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Opens goodbye note modal for users losing Grinder roles.', id: GOODBYE_NOTE_BUTTON_ID, - errorTag: () => `${GOODBYE_NOTE_BUTTON_ID}: ${ResetGrinderRoles.ERR.UnexpectedButton}`, + errorTag: () => `${moduleName}: ${ResetGrinderRoles.ERR.UnexpectedButton}`, async exec(_, interaction) { if (!interaction.isButton()) return diff --git a/src/bot/events/interaction-create/message/handlers/send-modal.ts b/src/bot/events/interaction-create/message/handlers/send-modal.ts index 4af351e..a1651dd 100644 --- a/src/bot/events/interaction-create/message/handlers/send-modal.ts +++ b/src/bot/events/interaction-create/message/handlers/send-modal.ts @@ -4,6 +4,7 @@ import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId, tempStore } from '@utils/component' import { getChannel, sendAsBot, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' import { Send } from '../validators/send' export class SendModalError extends DiscordBaseError { @@ -12,12 +13,13 @@ export class SendModalError extends DiscordBaseError { } } +const moduleName = getModuleName(EVENT_PATH, __filename) export const MESSAGE_SEND_ID = generateCustomId(EVENT_PATH, __filename) registerInteractionHandler({ desc: 'Handles message send modal submissions, posting messages (text/attachments) as the bot in the selected channel.', id: MESSAGE_SEND_ID, - errorTag: () => `${MESSAGE_SEND_ID}: ${Send.ERR.UnexpectedModal}`, + errorTag: () => `${moduleName}: ${Send.ERR.UnexpectedModal}`, async exec(_, interaction) { if (!interaction.isModalSubmit()) return From 0dd0f109eb5aabfccf2489c8d991e626165d0956 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 22:24:05 +0700 Subject: [PATCH 13/18] refactor: commands --- .../checkin/handlers/checkin-audit.ts | 6 +-- .../checkin/handlers/checkin-status.ts | 6 +-- src/bot/commands/checkin/handlers/checkin.ts | 6 +-- src/bot/commands/command.d.ts | 4 +- .../embed/handlers/role-grant-create.ts | 6 +-- src/bot/commands/index.ts | 37 +++++++++---------- src/bot/commands/message/handlers/send.ts | 6 +-- src/bot/commands/registry.ts | 8 ++++ src/bot/commands/utility/handlers/ping.ts | 6 +-- src/bot/events/index.ts | 2 +- src/index.ts | 8 ++-- 11 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 src/bot/commands/registry.ts diff --git a/src/bot/commands/checkin/handlers/checkin-audit.ts b/src/bot/commands/checkin/handlers/checkin-audit.ts index 10f1afc..17a0f3c 100644 --- a/src/bot/commands/checkin/handlers/checkin-audit.ts +++ b/src/bot/commands/checkin/handlers/checkin-audit.ts @@ -1,5 +1,5 @@ -import type { Command } from '@commands/command' import type { ChatInputCommandInteraction, Client } from 'discord.js' +import { registerCommand } from '@commands/registry' import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' import { CHECKIN_AUDIT_ID } from '@events/interaction-create/checkin/handlers/audit-modal' import { createCheckinReviewModal, encodeSnowflake, getCustomId } from '@utils/component' @@ -15,7 +15,7 @@ export class CheckinAuditError extends DiscordBaseError { } } -export default { +registerCommand({ data: new SlashCommandBuilder() .setName('checkin-audit') .setDescription('Review an old check-in using its public ID.') @@ -57,4 +57,4 @@ export default { else log.error(`Failed to handle: ${CheckinAudit.ERR.UnexpectedCheckinAudit}: ${err}`) } }, -} as Command +}) diff --git a/src/bot/commands/checkin/handlers/checkin-status.ts b/src/bot/commands/checkin/handlers/checkin-status.ts index 111cf39..909a5c2 100644 --- a/src/bot/commands/checkin/handlers/checkin-status.ts +++ b/src/bot/commands/checkin/handlers/checkin-status.ts @@ -1,5 +1,5 @@ -import type { Command } from '@commands/command' import type { ChatInputCommandInteraction, Client, GuildMember } from 'discord.js' +import { registerCommand } from '@commands/registry' import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE, GRINDER_ROLE } from '@config/discord' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' @@ -13,7 +13,7 @@ export class CheckinStatusError extends DiscordBaseError { } } -export default { +registerCommand({ data: new SlashCommandBuilder() .setName('checkin-status') .setDescription('Check your current daily check-in and streak status.'), @@ -47,4 +47,4 @@ export default { else log.error(`Failed to handle: ${CheckinStatus.ERR.UnexpectedCheckinStatus}: ${err}`) } }, -} as Command +}) diff --git a/src/bot/commands/checkin/handlers/checkin.ts b/src/bot/commands/checkin/handlers/checkin.ts index ab96010..859949f 100644 --- a/src/bot/commands/checkin/handlers/checkin.ts +++ b/src/bot/commands/checkin/handlers/checkin.ts @@ -1,5 +1,5 @@ -import type { Command } from '@commands/command' import type { ChatInputCommandInteraction } from 'discord.js' +import { registerCommand } from '@commands/registry' import { CHECKIN_CHANNEL } from '@config/discord' import { CHECKIN_ID } from '@events/interaction-create/checkin/handlers/modal' import { Checkin } from '@events/interaction-create/checkin/validators' @@ -16,7 +16,7 @@ export class CheckinError extends DiscordBaseError { } } -export default { +registerCommand({ data: new SlashCommandBuilder() .setName('checkin') .setDescription('Daily grind check-in.') @@ -67,4 +67,4 @@ export default { else log.error(`Failed to handle: ${Checkin.ERR.UnexpectedCheckin}: ${err}`) } }, -} as Command +}) diff --git a/src/bot/commands/command.d.ts b/src/bot/commands/command.d.ts index 4d80767..504bbc2 100644 --- a/src/bot/commands/command.d.ts +++ b/src/bot/commands/command.d.ts @@ -1,6 +1,6 @@ -import type { ChatInputCommandInteraction, Client, SlashCommandBuilder } from 'discord.js' +import type { ChatInputCommandInteraction, Client, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder } from 'discord.js' export interface Command { - data: SlashCommandBuilder + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder execute: (client: Client, interaction: ChatInputCommandInteraction) => Promise } diff --git a/src/bot/commands/embed/handlers/role-grant-create.ts b/src/bot/commands/embed/handlers/role-grant-create.ts index c8e36c0..4551ae2 100644 --- a/src/bot/commands/embed/handlers/role-grant-create.ts +++ b/src/bot/commands/embed/handlers/role-grant-create.ts @@ -1,5 +1,5 @@ -import type { Command } from '@commands/command' import type { ChatInputCommandInteraction, TextChannel } from 'discord.js' +import { registerCommand } from '@commands/registry' import { LabelBuilder, ModalBuilder, TextInputBuilder } from '@discordjs/builders' import { EMBED_ROLE_GRANT_CREATE_MODAL_ID } from '@events/interaction-create/embed/handlers/role-grant-create-modal' import { RoleGrantCreate } from '@events/interaction-create/embed/validators/role-grant-create' @@ -16,7 +16,7 @@ export class EmbedRoleGrantError extends DiscordBaseError { } } -export default { +registerCommand({ data: new SlashCommandBuilder() .setName('create-embed-role-grant') .setDescription('Create an embed in a channel w/ a role-grant button.') @@ -103,4 +103,4 @@ export default { else log.error(`Failed to handle ${EMBED_ROLE_GRANT_CREATE_MODAL_ID}: ${RoleGrantCreate.ERR.UnexpectedRoleGrantCreate}: ${err}`) } }, -} as Command +}) diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index b0d5f4a..a093298 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -1,9 +1,8 @@ -import type { Command } from '@commands/command' import type { Client } from 'discord.js' import path from 'node:path' -import { getModuleName, readFiles } from '@utils/io' +import { readFiles } from '@utils/io' import { log } from '@utils/logger' -import { Collection } from 'discord.js' +import { commandRegistry } from './registry' export class CommandError extends Error { constructor(message: string, options?: { cause?: unknown }) { @@ -14,24 +13,22 @@ export class CommandError extends Error { } export const COMMAND_PATH = path.basename(__dirname) -const files = readFiles(__dirname) -export async function registerCommands(client: Client) { - client.commands = new Collection() +export async function loadCommands(client: Client) { + const root = path.join(__dirname) + const files = readFiles(root) - for (const file of files) { - const fileName = getModuleName(COMMAND_PATH, file) - const { default: command } = await import(file) as { default: Command } - if (!command) - continue + await Promise.all( + files.map(async (file) => { + try { + await import(file) + log.info(`Loaded command file ${file}`) + } + catch (err) { + log.error(`Failed to load command file ${file}: ${err}`) + } + }), + ) - try { - log.info(`Registering command ${fileName}...`) - client.commands.set(command.data.name, command) - } - catch (err: any) { - const msg = err instanceof CommandError ? err.message : '❌ Something went wrong when importing the command' - log.error(`Failed to register a command: ${msg}: ${err.message}`) - } - } + client.commands = commandRegistry } diff --git a/src/bot/commands/message/handlers/send.ts b/src/bot/commands/message/handlers/send.ts index 542d510..87d1189 100644 --- a/src/bot/commands/message/handlers/send.ts +++ b/src/bot/commands/message/handlers/send.ts @@ -1,5 +1,5 @@ -import type { Command } from '@commands/command' import type { ChatInputCommandInteraction, TextChannel } from 'discord.js' +import { registerCommand } from '@commands/registry' import { MESSAGE_SEND_ID } from '@events/interaction-create/message/handlers/send-modal' import { Send } from '@events/interaction-create/message/validators/send' import { encodeSnowflake, getCustomId } from '@utils/component' @@ -29,7 +29,7 @@ for (let i = 1; i <= Send.ATTACHMENT_COUNT; i++) { ) } -export default { +registerCommand({ data, async execute(_, interaction: ChatInputCommandInteraction) { try { @@ -73,4 +73,4 @@ export default { else log.error(`Failed to handle: ${Send.ERR.UnexpectedSend}: ${err}`) } }, -} as Command +}) diff --git a/src/bot/commands/registry.ts b/src/bot/commands/registry.ts new file mode 100644 index 0000000..a850666 --- /dev/null +++ b/src/bot/commands/registry.ts @@ -0,0 +1,8 @@ +import type { Command } from '@commands/command' +import { Collection } from 'discord.js' + +export const commandRegistry = new Collection() + +export function registerCommand(command: Command) { + commandRegistry.set(command.data.name, command) +} diff --git a/src/bot/commands/utility/handlers/ping.ts b/src/bot/commands/utility/handlers/ping.ts index 3bea304..089d94d 100644 --- a/src/bot/commands/utility/handlers/ping.ts +++ b/src/bot/commands/utility/handlers/ping.ts @@ -1,5 +1,5 @@ -import type { Command } from '@commands/command' import type { ChatInputCommandInteraction, TextChannel } from 'discord.js' +import { registerCommand } from '@commands/registry' import { sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' @@ -12,7 +12,7 @@ export class PingError extends DiscordBaseError { } } -export default { +registerCommand({ data: new SlashCommandBuilder() .setName('ping') .setDescription('Replies with pong!'), @@ -33,4 +33,4 @@ export default { else log.error(`Failed to handle: ${Ping.ERR.UnexpectedPing}: ${err}`) } }, -} as Command +}) diff --git a/src/bot/events/index.ts b/src/bot/events/index.ts index bfa6b03..f20b1dc 100644 --- a/src/bot/events/index.ts +++ b/src/bot/events/index.ts @@ -15,7 +15,7 @@ export class EventError extends Error { export const EVENT_PATH = path.basename(__dirname) const files = readFiles(__dirname) -export async function registerEvents(client: Client) { +export async function loadEvents(client: Client) { for (const file of files) { const fileName = getModuleName(EVENT_PATH, file) const { default: event } = await import(file) as { default: Event } diff --git a/src/index.ts b/src/index.ts index 13c923d..ef004b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import process from 'node:process' -import { registerCommands } from '@commands/index' +import { loadCommands } from '@commands/index' import { prisma } from '@db/client' -import { registerEvents } from '@events/index' +import { loadEvents } from '@events/index' import { log } from '@utils/logger' import { Client, GatewayIntentBits, Partials } from 'discord.js' @@ -25,11 +25,11 @@ async function main() { log.base('πŸš€ Starting bot...') log.check('Loading events...') - await registerEvents(client) + await loadEvents(client) log.success('Events loaded~') log.check('Loading commands...') - await registerCommands(client) + await loadCommands(client) log.success('Commands loaded~') await client.login(process.env.APP_TOKEN) From bdda3780441cce1de33589dff85c43ac3be53096 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 22:27:24 +0700 Subject: [PATCH 14/18] chore: `generateCustomId` --- .../events/interaction-create/checkin/handlers/audit-modal.ts | 2 +- .../interaction-create/checkin/handlers/custom-button-modal.ts | 2 +- src/bot/events/interaction-create/checkin/handlers/modal.ts | 2 +- .../embed/handlers/role-grant-create-button.ts | 2 +- .../embed/handlers/role-grant-create-modal.ts | 2 +- .../events/interaction-create/message/handlers/send-modal.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts index 57ae212..842fc86 100644 --- a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -17,7 +17,7 @@ export class CheckinAuditModalError extends DiscordBaseError { } const moduleName = getModuleName(EVENT_PATH, __filename) -export const CHECKIN_AUDIT_ID = generateCustomId(EVENT_PATH, __filename) +export const CHECKIN_AUDIT_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles modal submissions for check-in audit modal forms.', diff --git a/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts b/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts index f696b31..0965780 100644 --- a/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button-modal.ts @@ -16,7 +16,7 @@ export class CheckinCustomButtonModalError extends DiscordBaseError { } const moduleName = getModuleName(EVENT_PATH, __filename) -export const CHECKIN_CUSTOM_BUTTON_MODAL_ID = generateCustomId(EVENT_PATH, __filename) +export const CHECKIN_CUSTOM_BUTTON_MODAL_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles modal submissions for the custom check-in review modal.', diff --git a/src/bot/events/interaction-create/checkin/handlers/modal.ts b/src/bot/events/interaction-create/checkin/handlers/modal.ts index 9be947f..2b1cb4f 100644 --- a/src/bot/events/interaction-create/checkin/handlers/modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/modal.ts @@ -15,7 +15,7 @@ export class CheckinModalError extends DiscordBaseError { } const moduleName = getModuleName(EVENT_PATH, __filename) -export const CHECKIN_ID = generateCustomId(EVENT_PATH, __filename) +export const CHECKIN_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles modal submissions for check-in modal forms.', diff --git a/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts b/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts index 0ec6287..9c4b35f 100644 --- a/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts +++ b/src/bot/events/interaction-create/embed/handlers/role-grant-create-button.ts @@ -14,7 +14,7 @@ export class EmbedRoleGrantButtonError extends DiscordBaseError { } const moduleName = getModuleName(EVENT_PATH, __filename) -export const EMBED_ROLE_GRANT_CREATE_BUTTON_ID = generateCustomId(EVENT_PATH, __filename) +export const EMBED_ROLE_GRANT_CREATE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles role assignment button interactions and adds a role for users.', diff --git a/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts b/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts index 64cf0bf..6dd72d8 100644 --- a/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts +++ b/src/bot/events/interaction-create/embed/handlers/role-grant-create-modal.ts @@ -15,7 +15,7 @@ export class EmbedRoleGrantModalError extends DiscordBaseError { } const moduleName = getModuleName(EVENT_PATH, __filename) -export const EMBED_ROLE_GRANT_CREATE_MODAL_ID = generateCustomId(EVENT_PATH, __filename) +export const EMBED_ROLE_GRANT_CREATE_MODAL_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles modal submissions for creating an embed with a role-grant button.', diff --git a/src/bot/events/interaction-create/message/handlers/send-modal.ts b/src/bot/events/interaction-create/message/handlers/send-modal.ts index a1651dd..8fe3f81 100644 --- a/src/bot/events/interaction-create/message/handlers/send-modal.ts +++ b/src/bot/events/interaction-create/message/handlers/send-modal.ts @@ -14,7 +14,7 @@ export class SendModalError extends DiscordBaseError { } const moduleName = getModuleName(EVENT_PATH, __filename) -export const MESSAGE_SEND_ID = generateCustomId(EVENT_PATH, __filename) +export const MESSAGE_SEND_ID = `${generateCustomId(EVENT_PATH, __filename)}` registerInteractionHandler({ desc: 'Handles message send modal submissions, posting messages (text/attachments) as the bot in the selected channel.', From a5e5cfbea07918a43c512120c61133d31abc8be7 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 22:54:46 +0700 Subject: [PATCH 15/18] refactor: `loadCommands` --- src/bot/commands/index.ts | 23 ++++------------------- src/bot/commands/registry.ts | 22 ++++++++++++++++++++++ src/bot/events/index.ts | 2 +- src/index.ts | 8 ++++---- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index a093298..633c7f7 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -1,8 +1,6 @@ import type { Client } from 'discord.js' import path from 'node:path' -import { readFiles } from '@utils/io' -import { log } from '@utils/logger' -import { commandRegistry } from './registry' +import { commandRegistry, loadCommands } from './registry' export class CommandError extends Error { constructor(message: string, options?: { cause?: unknown }) { @@ -12,23 +10,10 @@ export class CommandError extends Error { } } -export const COMMAND_PATH = path.basename(__dirname) +export const COMMAND_PATH = path.join(__dirname) -export async function loadCommands(client: Client) { - const root = path.join(__dirname) - const files = readFiles(root) - - await Promise.all( - files.map(async (file) => { - try { - await import(file) - log.info(`Loaded command file ${file}`) - } - catch (err) { - log.error(`Failed to load command file ${file}: ${err}`) - } - }), - ) +export async function registerCommands(client: Client) { + await loadCommands() client.commands = commandRegistry } diff --git a/src/bot/commands/registry.ts b/src/bot/commands/registry.ts index a850666..8ffd307 100644 --- a/src/bot/commands/registry.ts +++ b/src/bot/commands/registry.ts @@ -1,8 +1,30 @@ import type { Command } from '@commands/command' +import path from 'node:path' +import { readFiles } from '@utils/io' +import { log } from '@utils/logger' import { Collection } from 'discord.js' +import { COMMAND_PATH } from '.' export const commandRegistry = new Collection() export function registerCommand(command: Command) { commandRegistry.set(command.data.name, command) } + +export async function loadCommands() { + const files = readFiles(COMMAND_PATH).filter(file => !file.endsWith('/index.ts')) + + await Promise.all( + files.map(async (file) => { + const fileName = path.basename(file, path.extname(file)) + + try { + await import(file) + log.info(`Loaded command file '${fileName}'`) + } + catch (err) { + log.error(`Failed to load command file ${file}: ${err}`) + } + }), + ) +} diff --git a/src/bot/events/index.ts b/src/bot/events/index.ts index f20b1dc..bfa6b03 100644 --- a/src/bot/events/index.ts +++ b/src/bot/events/index.ts @@ -15,7 +15,7 @@ export class EventError extends Error { export const EVENT_PATH = path.basename(__dirname) const files = readFiles(__dirname) -export async function loadEvents(client: Client) { +export async function registerEvents(client: Client) { for (const file of files) { const fileName = getModuleName(EVENT_PATH, file) const { default: event } = await import(file) as { default: Event } diff --git a/src/index.ts b/src/index.ts index ef004b4..13c923d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import process from 'node:process' -import { loadCommands } from '@commands/index' +import { registerCommands } from '@commands/index' import { prisma } from '@db/client' -import { loadEvents } from '@events/index' +import { registerEvents } from '@events/index' import { log } from '@utils/logger' import { Client, GatewayIntentBits, Partials } from 'discord.js' @@ -25,11 +25,11 @@ async function main() { log.base('πŸš€ Starting bot...') log.check('Loading events...') - await loadEvents(client) + await registerEvents(client) log.success('Events loaded~') log.check('Loading commands...') - await loadCommands(client) + await registerCommands(client) log.success('Commands loaded~') await client.login(process.env.APP_TOKEN) From 48f432f0c3fab71b6a5b2b794672648c304be323 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 22:55:01 +0700 Subject: [PATCH 16/18] fix: deploy commands --- src/deploy-commands.ts | 44 ++++-------------------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index bde815d..3eb290c 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -1,49 +1,13 @@ -import type { Command } from '@commands/command' -import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord.js' -import path from 'node:path' import process from 'node:process' -import { getModuleName, readFiles } from '@utils/io' +import { commandRegistry, loadCommands } from '@commands/registry' import { log } from '@utils/logger' import { REST, Routes } from 'discord.js' -const root = path.join(__dirname, 'bot/commands') - -async function loadCommands(): Promise { - const files = readFiles(root) - +async function main() { log.base('πŸš€ Deploying commands...') - const results: Array = await Promise.all( - files.map(async (file) => { - const fileName = getModuleName(root, file) - const { default: command } = await import(file) as { default: Command } - if (!command) - return null - - try { - log.info(`Deploying command ${fileName}...`) - if ('data' in command && 'execute' in command) { - return command.data.toJSON() - } - else { - log.error(`The command at ${file} is missing a required "data" or "execute" property.`) - return null - } - } - catch (err) { - log.error(`Failed to import command at ${file}: ${err}`) - return null - } - }), - ) - - return results.filter( - (c): c is RESTPostAPIChatInputApplicationCommandsJSONBody => c !== null, - ) -} - -async function main() { - const commands = await loadCommands() + await loadCommands() + const commands = [...commandRegistry.values()].map(cmd => cmd.data.toJSON()) const rest = new REST().setToken(process.env.APP_TOKEN!) try { From a830352c9c9ceca36b1224206e7d12c31670193b Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 20 Dec 2025 22:55:14 +0700 Subject: [PATCH 17/18] feat: error handling for duplicate command --- src/bot/commands/registry.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bot/commands/registry.ts b/src/bot/commands/registry.ts index 8ffd307..49625f8 100644 --- a/src/bot/commands/registry.ts +++ b/src/bot/commands/registry.ts @@ -8,6 +8,10 @@ import { COMMAND_PATH } from '.' export const commandRegistry = new Collection() export function registerCommand(command: Command) { + if (commandRegistry.has(command.data.name)) { + throw new Error(`Duplicate command name: ${command.data.name}`) + } + commandRegistry.set(command.data.name, command) } From dcf9d962aa6ab11d809a74becfb138da33150073 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sun, 21 Dec 2025 11:54:37 +0700 Subject: [PATCH 18/18] chore: try-catch --- src/bot/commands/index.ts | 12 +++++++++--- src/bot/events/index.ts | 4 ++-- src/deploy-commands.ts | 8 ++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index 633c7f7..1a1209e 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -1,5 +1,6 @@ import type { Client } from 'discord.js' import path from 'node:path' +import { log } from '@utils/logger' import { commandRegistry, loadCommands } from './registry' export class CommandError extends Error { @@ -13,7 +14,12 @@ export class CommandError extends Error { export const COMMAND_PATH = path.join(__dirname) export async function registerCommands(client: Client) { - await loadCommands() - - client.commands = commandRegistry + try { + await loadCommands() + client.commands = commandRegistry + } + catch (err: any) { + const msg = err instanceof CommandError ? err.message : '❌ Something went wrong when importing the command' + log.error(`Failed to register an command: ${msg}: ${err.message}`) + } } diff --git a/src/bot/events/index.ts b/src/bot/events/index.ts index bfa6b03..3d90b68 100644 --- a/src/bot/events/index.ts +++ b/src/bot/events/index.ts @@ -13,9 +13,9 @@ export class EventError extends Error { } export const EVENT_PATH = path.basename(__dirname) -const files = readFiles(__dirname) - export async function registerEvents(client: Client) { + const files = readFiles(__dirname) + for (const file of files) { const fileName = getModuleName(EVENT_PATH, file) const { default: event } = await import(file) as { default: Event } diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 3eb290c..86b8c47 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -6,11 +6,11 @@ import { REST, Routes } from 'discord.js' async function main() { log.base('πŸš€ Deploying commands...') - await loadCommands() - const commands = [...commandRegistry.values()].map(cmd => cmd.data.toJSON()) - const rest = new REST().setToken(process.env.APP_TOKEN!) - try { + await loadCommands() + const commands = [...commandRegistry.values()].map(cmd => cmd.data.toJSON()) + const rest = new REST().setToken(process.env.APP_TOKEN!) + log.check(`Started refreshing ${commands.length} application (/) commands...`) const data = await rest.put(