From cb900fc6b97c4ee67c15b7c053a48670bebb0c98 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 21:33:28 +0700 Subject: [PATCH 01/17] chore: import --- src/bot/events/interaction-create/checkin/validators/audit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/events/interaction-create/checkin/validators/audit.ts b/src/bot/events/interaction-create/checkin/validators/audit.ts index 6881cdd..9f0f412 100644 --- a/src/bot/events/interaction-create/checkin/validators/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 { Checkin } from '.' import { CheckinAuditModalError } from '../handlers/audit-modal' import { CheckinAuditMessage } from '../messages/audit' -import { Checkin } from '.' export class CheckinAudit extends CheckinAuditMessage { static override BASE_PERMS = [ From cbe52e1ad1137b9b6229110dd325a2cabfa14213 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 21:55:38 +0700 Subject: [PATCH 02/17] fix: deleted func into react a message --- .../events/interaction-create/checkin/handlers/audit-modal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 842fc86..b86180b 100644 --- a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -51,7 +51,7 @@ registerInteractionHandler({ const { messageId } = CheckinAudit.getMessageFromLink(checkin.link!) const message = await checkinChannel.messages.fetch(messageId) - await Checkin.validateCheckinHandleSubmittedMsg(message, updatedCheckin, status) + await message.react(Checkin.REVERSED_EMOJI_STATUS[status]) } catch (err: any) { if (err instanceof DiscordBaseError) From bbf46ca523f4acb4377cc528788049c50b0b8528 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:06:40 +0700 Subject: [PATCH 03/17] chore: move granted role message into `MSG` --- .../embed/handlers/role-grant-create-button.ts | 2 +- src/utils/discord/assert.ts | 2 +- src/utils/discord/message.ts | 14 ++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) 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 9c4b35f..bed00cd 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 @@ -41,7 +41,7 @@ registerInteractionHandler({ await member.roles.add(role) await sendReply(interaction, ` - ${RoleGrantCreate.roleGranted(role.id)}`) + ${RoleGrantCreate.MSG.RoleGranted(role.id)}`) } catch (err: any) { if (err instanceof DiscordBaseError) diff --git a/src/utils/discord/assert.ts b/src/utils/discord/assert.ts index 936d4b3..d434dc9 100644 --- a/src/utils/discord/assert.ts +++ b/src/utils/discord/assert.ts @@ -77,7 +77,7 @@ export class DiscordAssert extends DiscordMessage { static assertMemberAlreadyHasRole(member: GuildMember, roleId: string) { if (this.isMemberHasRole(member, roleId)) - throw new DiscordAssertError(this.roleRevoked(roleId)) + throw new DiscordAssertError(this.MSG.RoleRevoked(roleId)) } static assertMemberHasRole(member: GuildMember, roleId: string) { diff --git a/src/utils/discord/message.ts b/src/utils/discord/message.ts index 720b1b2..5ff1014 100644 --- a/src/utils/discord/message.ts +++ b/src/utils/discord/message.ts @@ -35,13 +35,11 @@ export class DiscordMessage { ReachNewGrindRole(role: GrindRole) { return `🎉 You have reached a new grind role: <@&${(role.id)}>~` }, - } - - static roleGranted(roleId: string): string { - return `✅ Granted <@&${(roleId)}> to you` - } - - static roleRevoked(roleId: string): string { - return `❌ You already have the <@&${(roleId)}> role` + RoleGranted(roleId: string): string { + return `✅ Granted <@&${(roleId)}> to you` + }, + RoleRevoked(roleId: string): string { + return `❌ You already have the <@&${(roleId)}> role` + }, } } From e05462799f71fee953b1d7ffc88a2a5e2b1b76cb Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:16:11 +0700 Subject: [PATCH 04/17] feat: success audit message --- .../interaction-create/checkin/handlers/audit-modal.ts | 2 ++ src/bot/events/interaction-create/checkin/messages/audit.ts | 5 +++++ 2 files changed, 7 insertions(+) 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 b86180b..5d5f32d 100644 --- a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -52,6 +52,8 @@ registerInteractionHandler({ const { messageId } = CheckinAudit.getMessageFromLink(checkin.link!) const message = await checkinChannel.messages.fetch(messageId) await message.react(Checkin.REVERSED_EMOJI_STATUS[status]) + + await sendReply(interaction, CheckinAudit.MSG.AuditSuccess(checkin.link!, checkin.user!.discord_id)) } catch (err: any) { if (err instanceof DiscordBaseError) diff --git a/src/bot/events/interaction-create/checkin/messages/audit.ts b/src/bot/events/interaction-create/checkin/messages/audit.ts index a9b7a7e..eecca18 100644 --- a/src/bot/events/interaction-create/checkin/messages/audit.ts +++ b/src/bot/events/interaction-create/checkin/messages/audit.ts @@ -11,4 +11,9 @@ ${waitingCheckinList} `, UnexpectedCheckinAudit: '❌ Something went wrong during the check-in audit', } + + static override readonly MSG = { + ...DiscordAssert.MSG, + AuditSuccess: (msgLink: string, userDiscordId: string) => `✅ Successfully [audited check-in](${msgLink}) for <@${userDiscordId}>.`, + } } From e0eed54168c376ed1db8724a74fe020b3b0daf29 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:49:17 +0700 Subject: [PATCH 05/17] refactor: `validateCheckin` func --- .../checkin/handlers/approve-button.ts | 3 +-- .../checkin/handlers/custom-button-modal.ts | 6 ++--- .../checkin/handlers/custom-button.ts | 1 - .../checkin/handlers/reject-button.ts | 3 +-- .../checkin/validators/index.ts | 22 +++++++++++++------ .../checkin/handlers/submitted.ts | 3 +-- 6 files changed, 20 insertions(+), 18 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 7664cec..177b120 100644 --- a/src/bot/events/interaction-create/checkin/handlers/approve-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/approve-button.ts @@ -40,11 +40,10 @@ registerInteractionHandler({ Checkin.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE) await Checkin.validateCheckin( - client.prisma, + client, interaction.guild, flamewarden, { key: 'id', value: checkinId }, - interaction.message, 'APPROVED', ) } 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 0965780..e18c1c3 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 @@ -32,24 +32,22 @@ registerInteractionHandler({ if (!interaction.inCachedGuild()) throw new CheckinCustomButtonModalError(Checkin.ERR.NotGuild) - const { checkinId, messageId } = Checkin.getModalReviewId(interaction, interaction.customId) + const { checkinId } = Checkin.getModalReviewId(interaction, interaction.customId) const channel = interaction.channel as TextChannel Checkin.assertMissPerms(interaction.client.user, channel) const flamewarden = await interaction.guild.members.fetch(interaction.member.id) Checkin.assertMember(flamewarden) Checkin.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE) - const message = await channel.messages.fetch(messageId) const status = interaction.fields.getStringSelectValues('status')[0] as CheckinStatusType const comment = interaction.fields.getTextInputValue('comment') await Checkin.validateCheckin( - client.prisma, + client, interaction.guild, flamewarden, { key: 'id', value: checkinId }, - message, status, comment, ) 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 7fa3753..8677c1a 100644 --- a/src/bot/events/interaction-create/checkin/handlers/custom-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button.ts @@ -42,7 +42,6 @@ registerInteractionHandler({ CHECKIN_CUSTOM_BUTTON_MODAL_ID, encodeSnowflake(interaction.guildId), encodeSnowflake(checkinId.toString()), - encodeSnowflake(interaction.message.id), ]) const modal = createCheckinReviewModal(modalCustomId, checkin) 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 ae80cea..333112e 100644 --- a/src/bot/events/interaction-create/checkin/handlers/reject-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/reject-button.ts @@ -40,11 +40,10 @@ registerInteractionHandler({ Checkin.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE) await Checkin.validateCheckin( - client.prisma, + client, interaction.guild, flamewarden, { key: 'id', value: checkinId }, - interaction.message, 'REJECTED', ) } diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 68eca66..d56f44b 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -4,7 +4,7 @@ import type { Attachment as AttachmentType } from '@type/attachment' import type { CheckinAllowedEmojiType, CheckinColumn, CheckinStatusType, Checkin as CheckinType } from '@type/checkin' import type { CheckinStreak } from '@type/checkin-streak' import type { User } from '@type/user' -import type { Attachment, EmbedBuilder, Guild, GuildMember, Interaction, Message } from 'discord.js' +import type { Attachment, Client, EmbedBuilder, Guild, GuildMember, Interaction, Message, TextChannel } from 'discord.js' import crypto from 'node:crypto' import { CheckinError } from '@commands/checkin/handlers/checkin' import { AURA_FARMING_CHANNEL, CHECKIN_CHANNEL, GRINDER_ROLE } from '@config/discord' @@ -401,20 +401,27 @@ export class Checkin extends CheckinMessage { } static async validateCheckin( - prisma: PrismaClient, + client: Client, guild: Guild, flamewarden: GuildMember, opt: CheckinColumn, - message: Message, checkinStatus: CheckinStatusType, comment?: string | null, - ) { - const checkin = await this.getWaitingCheckin(prisma, opt.key, opt.value) - this.assertSubmittedCheckinToday(checkin) - const updatedCheckin = await this.updateCheckinStatus(prisma, flamewarden, checkin, checkinStatus, comment) as CheckinType + isAudit: boolean = false, + ): Promise { + const checkin = await this.getWaitingCheckin(client.prisma, opt.key, opt.value) + if (!isAudit) + this.assertSubmittedCheckinToday(checkin) + const updatedCheckin = await this.updateCheckinStatus(client.prisma, flamewarden, checkin, checkinStatus, comment) as CheckinType + + const checkinChannel = await client.channels.fetch(CHECKIN_CHANNEL) as TextChannel + const { messageId } = this.getMessageFromLink(checkin.link!) + const message = await checkinChannel.messages.fetch(messageId) await this.validateCheckinHandleToUser(guild, flamewarden, checkin.user!.discord_id, updatedCheckin) await message.react(this.REVERSED_EMOJI_STATUS[checkinStatus]) + + return updatedCheckin } static async validateCheckinHandleToUser(guild: Guild, flamewarden: GuildMember, userDiscordId: string, updatedCheckin: CheckinType) { @@ -462,6 +469,7 @@ export class Checkin extends CheckinMessage { }, }, include: { + user: true, checkin_streak: true, }, }) 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 fdd185d..5d1ac2a 100644 --- a/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts +++ b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts @@ -36,11 +36,10 @@ registerReactionHandler({ await Checkin.assertAllowedChannel(guild, message.channel.id, CHECKIN_CHANNEL) await Checkin.validateCheckin( - client.prisma, + client, guild, flamewarden, { key: 'link', value: message.url }, - message, Checkin.EMOJI_STATUS[emoji], ) } From 172ca3ebbc450a01b8957d9eaeb8a58302c7f579 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:49:43 +0700 Subject: [PATCH 06/17] refactor: audit a checkin use `validateCheckin` func instead --- .../checkin/handlers/audit-modal.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 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 5d5f32d..4c5e58f 100644 --- a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -1,6 +1,6 @@ import type { CheckinStatusType } from '@type/checkin' import type { TextChannel } from 'discord.js' -import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' +import { FLAMEWARDEN_ROLE } from '@config/discord' import { EVENT_PATH } from '@events/index' import { registerInteractionHandler } from '@events/interaction-create/registry' import { generateCustomId } from '@utils/component' @@ -36,7 +36,6 @@ registerInteractionHandler({ const { checkinId } = CheckinAudit.getModalReviewId(interaction, interaction.customId) const channel = interaction.channel as TextChannel - const checkinChannel = await interaction.client.channels.fetch(CHECKIN_CHANNEL) as TextChannel CheckinAudit.assertMissPerms(interaction.client.user, channel) const flamewarden = await interaction.guild.members.fetch(interaction.member.id) CheckinAudit.assertMember(flamewarden) @@ -45,15 +44,17 @@ registerInteractionHandler({ const status: CheckinStatusType = 'APPROVED' const comment = interaction.fields.getTextInputValue('comment') - const checkin = await Checkin.getWaitingCheckin(client.prisma, 'public_id', checkinId) - const updatedCheckin = await Checkin.updateCheckinStatus(client.prisma, flamewarden, checkin, status, comment, true) - await Checkin.validateCheckinHandleToUser(interaction.guild, flamewarden, checkin.user!.discord_id, updatedCheckin) + const updatedCheckin = await Checkin.validateCheckin( + client, + interaction.guild, + flamewarden, + { key: 'public_id', value: checkinId }, + status, + comment, + true, + ) - const { messageId } = CheckinAudit.getMessageFromLink(checkin.link!) - const message = await checkinChannel.messages.fetch(messageId) - await message.react(Checkin.REVERSED_EMOJI_STATUS[status]) - - await sendReply(interaction, CheckinAudit.MSG.AuditSuccess(checkin.link!, checkin.user!.discord_id)) + await sendReply(interaction, CheckinAudit.MSG.AuditSuccess(updatedCheckin.link!, updatedCheckin.user!.discord_id)) } catch (err: any) { if (err instanceof DiscordBaseError) From 905d98616ea398137f3214a1dca76673886d97ee Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:49:56 +0700 Subject: [PATCH 07/17] chore: remove unused value --- .../events/interaction-create/checkin/validators/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index d56f44b..9dbda1f 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -54,7 +54,7 @@ export class Checkin extends CheckinMessage { } static getModalReviewId(interaction: Interaction, customId: string) { - const [prefix, guildId, checkinId, messageId] = decodeSnowflakes(customId) + const [prefix, guildId, checkinId] = decodeSnowflakes(customId) const checkinIdNum = Number(checkinId) if (!guildId) @@ -63,10 +63,8 @@ export class Checkin extends CheckinMessage { throw new CheckinCustomButtonModalError(this.ERR.NotGuild) if (!checkinId) throw new CheckinCustomButtonModalError(this.ERR.CheckinIdMissing) - if (!messageId) - throw new CheckinCustomButtonModalError(this.ERR.MessageIdMissing) - return { prefix, guildId, checkinId: checkinIdNum, messageId } + return { prefix, guildId, checkinId: checkinIdNum } } static getButtonId(interaction: Interaction, customId: string) { From 6a32ab978722b36e26d41e2431f60c917e35c598 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:52:50 +0700 Subject: [PATCH 08/17] refactor: use `isMemberHasRole` func instead --- .../events/client-ready/jobs/validators/reset-grinder-roles.ts | 2 +- src/bot/events/interaction-create/checkin/validators/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 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 75e1017..def0b3d 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 @@ -55,7 +55,7 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { const grindRoles = getGrindRoles() for (const grindRole of grindRoles) { - if (member.roles.cache.has(grindRole.id)) { + if (this.isMemberHasRole(member, grindRole.id)) { await member.roles.remove(grindRole.id) } } diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 9dbda1f..7a1df8a 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -138,7 +138,7 @@ export class Checkin extends CheckinMessage { if (!newRole) return - const alreadyHasRole = member.roles.cache.has(newRole.id) + const alreadyHasRole = this.isMemberHasRole(member, newRole.id) const channel = await getChannel(guild, AURA_FARMING_CHANNEL) this.assertChannel(channel) @@ -425,6 +425,7 @@ export class Checkin extends CheckinMessage { static async validateCheckinHandleToUser(guild: Guild, flamewarden: GuildMember, userDiscordId: string, updatedCheckin: CheckinType) { const member = await guild.members.fetch(userDiscordId) this.assertMember(member) + const newGrindRole = this.getNewGrindRole(guild, updatedCheckin.checkin_streak!.streak) await this.setMemberNewGrindRole(guild, member, newGrindRole) await this.sendCheckinStatusToMember(flamewarden, member, updatedCheckin) From 0affbc992e101dcce1e89d9c06aeecf2171a48fc Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:53:50 +0700 Subject: [PATCH 09/17] chore: var naming --- src/bot/events/interaction-create/checkin/validators/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 7a1df8a..1d6f628 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -138,11 +138,11 @@ export class Checkin extends CheckinMessage { if (!newRole) return - const alreadyHasRole = this.isMemberHasRole(member, newRole.id) + const hasGrindRole = this.isMemberHasRole(member, newRole.id) const channel = await getChannel(guild, AURA_FARMING_CHANNEL) this.assertChannel(channel) - if (!alreadyHasRole) { + if (!hasGrindRole) { await attachNewGrindRole(member, newRole) await sendAsBot(null, channel, { content: `**Congratulations, <@${member.id}>** ${this.MSG.ReachNewGrindRole(newRole)}`, From 0b993c2c7978c1e90f1dd3c8aaf800b893b6dabb Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 22:59:36 +0700 Subject: [PATCH 10/17] feat: add grinder role if member does not have --- src/bot/events/interaction-create/checkin/validators/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 1d6f628..d341a1a 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -426,6 +426,10 @@ export class Checkin extends CheckinMessage { const member = await guild.members.fetch(userDiscordId) this.assertMember(member) + const hasGrinderRole = this.isMemberHasRole(member, GRINDER_ROLE) + if (!hasGrinderRole) + await member.roles.add(GRINDER_ROLE) + const newGrindRole = this.getNewGrindRole(guild, updatedCheckin.checkin_streak!.streak) await this.setMemberNewGrindRole(guild, member, newGrindRole) await this.sendCheckinStatusToMember(flamewarden, member, updatedCheckin) From a9bcd31718d80153a7a6cc21c01d56c226a61081 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 23:13:22 +0700 Subject: [PATCH 11/17] feat: set `streak_broken_at` to `null` after audit the checkin --- .../events/interaction-create/checkin/validators/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index d341a1a..fa13807 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -410,7 +410,7 @@ export class Checkin extends CheckinMessage { const checkin = await this.getWaitingCheckin(client.prisma, opt.key, opt.value) if (!isAudit) this.assertSubmittedCheckinToday(checkin) - const updatedCheckin = await this.updateCheckinStatus(client.prisma, flamewarden, checkin, checkinStatus, comment) as CheckinType + const updatedCheckin = await this.updateCheckinStatus(client.prisma, flamewarden, checkin, checkinStatus, comment, isAudit) as CheckinType const checkinChannel = await client.channels.fetch(CHECKIN_CHANNEL) as TextChannel const { messageId } = this.getMessageFromLink(checkin.link!) @@ -450,9 +450,9 @@ export class Checkin extends CheckinMessage { checkin: CheckinType, checkinStatus: CheckinStatusType, comment: string | null = null, - isLateCheckin: boolean = false, + isAudit: boolean = false, ): Promise { - const updatedDate = isLateCheckin ? checkin.created_at : new Date() + const updatedDate = isAudit ? checkin.created_at : new Date() const updatedCheckin = await prisma.checkin.update({ where: { id: checkin.id }, @@ -468,6 +468,7 @@ export class Checkin extends CheckinMessage { }, last_date: updatedDate, updated_at: updatedDate, + streak_broken_at: null, }, }, }, From a07439bea1039219b937929fe9eff60cb52f159f Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 23:22:26 +0700 Subject: [PATCH 12/17] chore: remove unused include --- .../events/interaction-create/checkin/validators/audit.ts | 1 - .../events/interaction-create/checkin/validators/index.ts | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bot/events/interaction-create/checkin/validators/audit.ts b/src/bot/events/interaction-create/checkin/validators/audit.ts index 9f0f412..408cd12 100644 --- a/src/bot/events/interaction-create/checkin/validators/audit.ts +++ b/src/bot/events/interaction-create/checkin/validators/audit.ts @@ -67,7 +67,6 @@ ${checkin.public_id} static async assertExistCheckinId(prisma: PrismaClient, checkinId: string) { const checkin = await prisma.checkin.findUnique({ where: { public_id: checkinId }, - include: { user: true, checkin_streak: true }, }) if (!checkin) { throw new CheckinAuditError(this.ERR.CheckinIdInvalid) diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index fa13807..0edea96 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -472,10 +472,7 @@ export class Checkin extends CheckinMessage { }, }, }, - include: { - user: true, - checkin_streak: true, - }, + include: { user: true }, }) return updatedCheckin From 648c3f1c8bb67a2d2f6d9550bef4138180e1efe9 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 23:28:13 +0700 Subject: [PATCH 13/17] fix: need this include AHHAHAHAHAHAH --- src/bot/events/interaction-create/checkin/validators/audit.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bot/events/interaction-create/checkin/validators/audit.ts b/src/bot/events/interaction-create/checkin/validators/audit.ts index 408cd12..9f0f412 100644 --- a/src/bot/events/interaction-create/checkin/validators/audit.ts +++ b/src/bot/events/interaction-create/checkin/validators/audit.ts @@ -67,6 +67,7 @@ ${checkin.public_id} static async assertExistCheckinId(prisma: PrismaClient, checkinId: string) { const checkin = await prisma.checkin.findUnique({ where: { public_id: checkinId }, + include: { user: true, checkin_streak: true }, }) if (!checkin) { throw new CheckinAuditError(this.ERR.CheckinIdInvalid) From e7d0009a3a8744a5ed44253b3a5812da0e7612f3 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Wed, 24 Dec 2025 23:29:14 +0700 Subject: [PATCH 14/17] NEED THIS ONE TOO WKWKWKWKWKWK TDK JLS --- src/bot/events/interaction-create/checkin/validators/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 0edea96..4d56d97 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -472,7 +472,7 @@ export class Checkin extends CheckinMessage { }, }, }, - include: { user: true }, + include: { user: true, checkin_streak: true }, }) return updatedCheckin From dbf33b16abcbfeb71439be46bb78c514658d19c7 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Thu, 25 Dec 2025 00:27:29 +0700 Subject: [PATCH 15/17] feat: checkin created at err and checkin where unique input --- src/types/checkin.d.ts | 2 +- src/utils/discord/message.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/checkin.d.ts b/src/types/checkin.d.ts index ee00121..521ef8d 100644 --- a/src/types/checkin.d.ts +++ b/src/types/checkin.d.ts @@ -26,5 +26,5 @@ export interface Checkin { export interface CheckinColumn { key: T - value: Prisma.CheckinWhereInput[T] + value: Prisma.CheckinWhereInput[T] | Prisma.CheckinWhereUniqueInput[K] } diff --git a/src/utils/discord/message.ts b/src/utils/discord/message.ts index 5ff1014..ac35589 100644 --- a/src/utils/discord/message.ts +++ b/src/utils/discord/message.ts @@ -25,6 +25,8 @@ export class DiscordMessage { PlainMessage: '❌ There is nothing to do with this plain message', CheckinIdMissing: '❌ Check-in ID is missing or invalid', CheckinIdInvalid: '❌ The provided check-in ID is invalid', + CheckinDateMissing: '❌ Check-in date is missing or invalid', + CheckinDateInvalid: '❌ The check-in date is invalid', UnexpectedModal: '❌ Something went wrong while handling the modal component', UnexpectedButton: '❌ Something went wrong while handling the button component', From 57564f5bf3fd246b0068ae5ed7833fc7ab15fea0 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Thu, 25 Dec 2025 00:28:16 +0700 Subject: [PATCH 16/17] refactor: buttons and modals with `checkinCreatedAt` data --- .../checkin/handlers/checkin-audit.ts | 1 + .../checkin/handlers/approve-button.ts | 3 +- .../checkin/handlers/audit-modal.ts | 3 +- .../checkin/handlers/custom-button-modal.ts | 3 +- .../checkin/handlers/custom-button.ts | 3 +- .../checkin/handlers/modal.ts | 2 +- .../checkin/handlers/reject-button.ts | 3 +- .../checkin/validators/audit.ts | 10 ++- .../checkin/validators/index.ts | 63 ++++++++++++------- .../checkin/handlers/submitted.ts | 1 + 10 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/bot/commands/checkin/handlers/checkin-audit.ts b/src/bot/commands/checkin/handlers/checkin-audit.ts index 17a0f3c..d3ff30a 100644 --- a/src/bot/commands/checkin/handlers/checkin-audit.ts +++ b/src/bot/commands/checkin/handlers/checkin-audit.ts @@ -46,6 +46,7 @@ registerCommand({ CHECKIN_AUDIT_ID, encodeSnowflake(interaction.guildId), checkinId, + checkin.created_at.getTime().toString(), ]) const modal = createCheckinReviewModal(modalCustomId, checkin, false) 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 177b120..728ea5a 100644 --- a/src/bot/events/interaction-create/checkin/handlers/approve-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/approve-button.ts @@ -31,7 +31,7 @@ registerInteractionHandler({ if (!interaction.inCachedGuild()) throw new CheckinApproveButtonError(Checkin.ERR.NotGuild) - const { checkinId } = Checkin.getButtonId(interaction, interaction.customId) + const { checkinId, checkinCreatedAt } = Checkin.getButtonId(interaction, interaction.customId) const channel = interaction.channel as TextChannel Checkin.assertMissPerms(interaction.client.user, channel) @@ -44,6 +44,7 @@ registerInteractionHandler({ interaction.guild, flamewarden, { key: 'id', value: checkinId }, + checkinCreatedAt, 'APPROVED', ) } 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 4c5e58f..b1437ab 100644 --- a/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/audit-modal.ts @@ -33,7 +33,7 @@ registerInteractionHandler({ if (!interaction.inCachedGuild()) throw new CheckinAuditModalError(CheckinAudit.ERR.NotGuild) - const { checkinId } = CheckinAudit.getModalReviewId(interaction, interaction.customId) + const { checkinId, checkinCreatedAt } = CheckinAudit.getModalReviewId(interaction, interaction.customId) const channel = interaction.channel as TextChannel CheckinAudit.assertMissPerms(interaction.client.user, channel) @@ -49,6 +49,7 @@ registerInteractionHandler({ interaction.guild, flamewarden, { key: 'public_id', value: checkinId }, + checkinCreatedAt, status, comment, true, 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 e18c1c3..b239bf8 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 @@ -32,7 +32,7 @@ registerInteractionHandler({ if (!interaction.inCachedGuild()) throw new CheckinCustomButtonModalError(Checkin.ERR.NotGuild) - const { checkinId } = Checkin.getModalReviewId(interaction, interaction.customId) + const { checkinId, checkinCreatedAt } = Checkin.getModalReviewId(interaction, interaction.customId) const channel = interaction.channel as TextChannel Checkin.assertMissPerms(interaction.client.user, channel) @@ -48,6 +48,7 @@ registerInteractionHandler({ interaction.guild, flamewarden, { key: 'id', value: checkinId }, + checkinCreatedAt, status, comment, ) 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 8677c1a..62cdfba 100644 --- a/src/bot/events/interaction-create/checkin/handlers/custom-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/custom-button.ts @@ -36,12 +36,13 @@ registerInteractionHandler({ Checkin.assertMember(flamewarden) Checkin.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE) - const { checkinId } = Checkin.getButtonId(interaction, interaction.customId) + const { checkinId, checkinCreatedAt } = Checkin.getButtonId(interaction, interaction.customId) const checkin = await Checkin.getWaitingCheckin(client.prisma, 'id', checkinId) const modalCustomId = getCustomId([ CHECKIN_CUSTOM_BUTTON_MODAL_ID, encodeSnowflake(interaction.guildId), encodeSnowflake(checkinId.toString()), + checkinCreatedAt.getTime().toString(), ]) const modal = createCheckinReviewModal(modalCustomId, checkin) diff --git a/src/bot/events/interaction-create/checkin/handlers/modal.ts b/src/bot/events/interaction-create/checkin/handlers/modal.ts index aae08ed..ff52429 100644 --- a/src/bot/events/interaction-create/checkin/handlers/modal.ts +++ b/src/bot/events/interaction-create/checkin/handlers/modal.ts @@ -43,7 +43,7 @@ registerInteractionHandler({ Checkin.assertCheckinToday(user) const { checkin } = await Checkin.validateCheckinStreak(client.prisma, user.id, user.checkin_streaks?.[0], todo) - const buttons = Checkin.generateButtons(interaction.guildId, checkin.id.toString()) + const buttons = Checkin.generateButtons(interaction.guildId, checkin.id.toString(), checkin.created_at) const msg = await sendReply( interaction, 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 333112e..a5db077 100644 --- a/src/bot/events/interaction-create/checkin/handlers/reject-button.ts +++ b/src/bot/events/interaction-create/checkin/handlers/reject-button.ts @@ -31,7 +31,7 @@ registerInteractionHandler({ if (!interaction.inCachedGuild()) throw new CheckinRejectButtonError(Checkin.ERR.NotGuild) - const { checkinId } = Checkin.getButtonId(interaction, interaction.customId) + const { checkinId, checkinCreatedAt } = Checkin.getButtonId(interaction, interaction.customId) const channel = interaction.channel as TextChannel Checkin.assertMissPerms(interaction.client.user, channel) @@ -44,6 +44,7 @@ registerInteractionHandler({ interaction.guild, flamewarden, { key: 'id', value: checkinId }, + checkinCreatedAt, 'REJECTED', ) } diff --git a/src/bot/events/interaction-create/checkin/validators/audit.ts b/src/bot/events/interaction-create/checkin/validators/audit.ts index 9f0f412..713916a 100644 --- a/src/bot/events/interaction-create/checkin/validators/audit.ts +++ b/src/bot/events/interaction-create/checkin/validators/audit.ts @@ -18,7 +18,7 @@ export class CheckinAudit extends CheckinAuditMessage { ] static getModalReviewId(interaction: Interaction, customId: string) { - const [prefix, guildId, checkinId] = decodeSnowflakes(customId) + const [prefix, guildId, checkinId, checkinTs] = decodeSnowflakes(customId) if (!guildId) throw new CheckinAuditModalError(this.ERR.GuildMissing) @@ -26,8 +26,14 @@ export class CheckinAudit extends CheckinAuditMessage { throw new CheckinAuditModalError(this.ERR.NotGuild) if (!checkinId) throw new CheckinAuditModalError(this.ERR.CheckinIdMissing) + if (!checkinTs) + throw new CheckinAuditModalError(this.ERR.CheckinDateMissing) - return { prefix, guildId, checkinId } + const checkinCreatedAt = new Date(Number(checkinTs)) + if (!checkinCreatedAt) + throw new CheckinAuditModalError(this.ERR.CheckinDateInvalid) + + return { prefix, guildId, checkinId, checkinCreatedAt } } static assertCheckinNotToday(checkin: CheckinType) { diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 4d56d97..25d85b1 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -54,8 +54,7 @@ export class Checkin extends CheckinMessage { } static getModalReviewId(interaction: Interaction, customId: string) { - const [prefix, guildId, checkinId] = decodeSnowflakes(customId) - const checkinIdNum = Number(checkinId) + const [prefix, guildId, checkinId, checkinTs] = decodeSnowflakes(customId) if (!guildId) throw new CheckinCustomButtonModalError(this.ERR.GuildMissing) @@ -63,13 +62,22 @@ export class Checkin extends CheckinMessage { throw new CheckinCustomButtonModalError(this.ERR.NotGuild) if (!checkinId) throw new CheckinCustomButtonModalError(this.ERR.CheckinIdMissing) + if (!checkinTs) + throw new CheckinCustomButtonModalError(this.ERR.CheckinDateMissing) + + const checkinIdNum = Number(checkinId) + if (Number.isNaN(checkinIdNum)) + throw new CheckinError(this.ERR.CheckinIdInvalid) + + const checkinCreatedAt = new Date(Number(checkinTs)) + if (!checkinCreatedAt) + throw new CheckinCustomButtonModalError(this.ERR.CheckinDateInvalid) - return { prefix, guildId, checkinId: checkinIdNum } + return { prefix, guildId, checkinId: checkinIdNum, checkinCreatedAt } } static getButtonId(interaction: Interaction, customId: string) { - const [prefix, guildId, checkinId] = decodeSnowflakes(customId) - const checkinIdNum = Number(checkinId) + const [prefix, guildId, checkinId, checkinTs] = decodeSnowflakes(customId) if (!guildId) throw new CheckinError(this.ERR.GuildMissing) @@ -77,10 +85,18 @@ export class Checkin extends CheckinMessage { throw new CheckinError(this.ERR.NotGuild) if (!checkinId) throw new CheckinError(this.ERR.CheckinIdMissing) + if (!checkinTs) + throw new CheckinError(this.ERR.CheckinDateMissing) + + const checkinIdNum = Number(checkinId) if (Number.isNaN(checkinIdNum)) throw new CheckinError(this.ERR.CheckinIdInvalid) - return { prefix, guildId, checkinId: checkinIdNum } + const checkinCreatedAt = new Date(Number(checkinTs)) + if (!checkinCreatedAt) + throw new CheckinError(this.ERR.CheckinDateInvalid) + + return { prefix, guildId, checkinId: checkinIdNum, checkinCreatedAt } } static generatePublicId(): string { @@ -98,26 +114,26 @@ export class Checkin extends CheckinMessage { } } - static generateButtons(guildId: string, checkinId: string): ActionRowBuilder { - const detailButtonId = getCustomId([CHECKIN_DETAIL_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId)]) + static generateButtons(guildId: string, checkinId: string, checkinCreatedAt: Date): ActionRowBuilder { + const detailButtonId = getCustomId([CHECKIN_DETAIL_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId), checkinCreatedAt.getTime().toString()]) const detailButton = new ButtonBuilder() .setCustomId(detailButtonId) .setLabel('🔍 Detail') .setStyle(ButtonStyle.Primary) - const approveButtonId = getCustomId([CHECKIN_APPROVE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId)]) + const approveButtonId = getCustomId([CHECKIN_APPROVE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId), checkinCreatedAt.getTime().toString()]) const approveButton = new ButtonBuilder() .setCustomId(approveButtonId) .setLabel('🔥 Approve') .setStyle(ButtonStyle.Success) - const rejectButtonId = getCustomId([CHECKIN_REJECT_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId)]) + const rejectButtonId = getCustomId([CHECKIN_REJECT_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId), checkinCreatedAt.getTime().toString()]) const rejectButton = new ButtonBuilder() .setCustomId(rejectButtonId) .setLabel('🙅 Reject') .setStyle(ButtonStyle.Danger) - const customButtonId = getCustomId([CHECKIN_CUSTOM_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId)]) + const customButtonId = getCustomId([CHECKIN_CUSTOM_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(checkinId), checkinCreatedAt.getTime().toString()]) const customButton = new ButtonBuilder() .setCustomId(customButtonId) .setLabel('⚙️ Review') @@ -169,7 +185,9 @@ export class Checkin extends CheckinMessage { throw new CheckinModalError(this.ERR.AlreadyCheckinToday(latestCheckin!.link!)) } - static assertSubmittedCheckinToday(checkin: CheckinType) { + static async assertSubmittedCheckinToday(prisma: PrismaClient, opt: CheckinColumn) { + const checkin = await this.getWaitingCheckin(prisma, opt.key, opt.value) + const isCheckinToday = this.hasCheckinToday(checkin.checkin_streak, checkin) if (!isCheckinToday) throw new SubmittedCheckinError(this.ERR.SubmittedCheckinNotToday(checkin.link!)) @@ -403,20 +421,20 @@ export class Checkin extends CheckinMessage { guild: Guild, flamewarden: GuildMember, opt: CheckinColumn, + checkinCreatedAt: Date, checkinStatus: CheckinStatusType, comment?: string | null, isAudit: boolean = false, ): Promise { - const checkin = await this.getWaitingCheckin(client.prisma, opt.key, opt.value) if (!isAudit) - this.assertSubmittedCheckinToday(checkin) - const updatedCheckin = await this.updateCheckinStatus(client.prisma, flamewarden, checkin, checkinStatus, comment, isAudit) as CheckinType + await this.assertSubmittedCheckinToday(client.prisma, opt) + const updatedCheckin = await this.updateCheckinStatus(client.prisma, flamewarden, opt, checkinCreatedAt, checkinStatus, comment, isAudit) as CheckinType const checkinChannel = await client.channels.fetch(CHECKIN_CHANNEL) as TextChannel - const { messageId } = this.getMessageFromLink(checkin.link!) + const { messageId } = this.getMessageFromLink(updatedCheckin.link!) const message = await checkinChannel.messages.fetch(messageId) - await this.validateCheckinHandleToUser(guild, flamewarden, checkin.user!.discord_id, updatedCheckin) + await this.validateCheckinHandleToUser(guild, flamewarden, updatedCheckin.user!.discord_id, updatedCheckin) await message.react(this.REVERSED_EMOJI_STATUS[checkinStatus]) return updatedCheckin @@ -444,18 +462,21 @@ export class Checkin extends CheckinMessage { }) } - static async updateCheckinStatus( + static async updateCheckinStatus( prisma: PrismaClient, member: GuildMember, - checkin: CheckinType, + opt: CheckinColumn, + checkinCreatedAt: Date, checkinStatus: CheckinStatusType, comment: string | null = null, isAudit: boolean = false, ): Promise { - const updatedDate = isAudit ? checkin.created_at : new Date() + const updatedDate = isAudit ? checkinCreatedAt : new Date() const updatedCheckin = await prisma.checkin.update({ - where: { id: checkin.id }, + where: { + [opt.key!]: opt.value!, + } as Prisma.CheckinWhereUniqueInput, data: { status: checkinStatus, reviewed_by: member.id, 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 5d1ac2a..6215cac 100644 --- a/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts +++ b/src/bot/events/message-reaction-add/checkin/handlers/submitted.ts @@ -40,6 +40,7 @@ registerReactionHandler({ guild, flamewarden, { key: 'link', value: message.url }, + message.createdAt, Checkin.EMOJI_STATUS[emoji], ) } From b21a6f780fb678bab5c219e4fc20a2bf6807a1d3 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Thu, 25 Dec 2025 00:28:29 +0700 Subject: [PATCH 17/17] fix: link column must be unique --- db/migrations/20250810031908_init/migration.sql | 2 +- db/schema.prisma | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrations/20250810031908_init/migration.sql b/db/migrations/20250810031908_init/migration.sql index f01eaeb..588e9bc 100644 --- a/db/migrations/20250810031908_init/migration.sql +++ b/db/migrations/20250810031908_init/migration.sql @@ -24,7 +24,7 @@ CREATE TABLE "public"."Checkin" ( "user_id" INTEGER NOT NULL, "checkin_streak_id" INTEGER NOT NULL, "description" TEXT NOT NULL, - "link" TEXT, + "link" TEXT UNIQUE, "status" TEXT NOT NULL DEFAULT 'WAITING', "reviewed_by" TEXT, "comment" TEXT, diff --git a/db/schema.prisma b/db/schema.prisma index b63aff1..ef20bc4 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -38,7 +38,7 @@ model Checkin { user_id Int checkin_streak_id Int description String - link String? + link String? @unique status String reviewed_by String? comment String?