From b444e0bfd91d9b1532b391c8bf4a496aa4581605 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 3 Jan 2026 20:48:05 +0700 Subject: [PATCH 1/6] feat: resolved checkin state --- .../checkin/validators/checkin-status.ts | 21 ++++++++++++++++++- src/types/checkin.d.ts | 6 ++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/bot/commands/checkin/validators/checkin-status.ts b/src/bot/commands/checkin/validators/checkin-status.ts index e4085d1..8caf1c6 100644 --- a/src/bot/commands/checkin/validators/checkin-status.ts +++ b/src/bot/commands/checkin/validators/checkin-status.ts @@ -1,5 +1,5 @@ import type { PrismaClient } from '@generatedDB/client' -import type { CheckinStatusType, Checkin as CheckinType } from '@type/checkin' +import type { CheckinStatusType, Checkin as CheckinType, ResolvedCheckinState } from '@type/checkin' import type { User } from '@type/user' import type { EmbedBuilder, Guild, Interaction, ThreadAutoArchiveDuration } from 'discord.js' import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' @@ -37,6 +37,25 @@ export class CheckinStatus extends CheckinStatusMessage { return { prefix, guildId, checkinLink } } + static resolveCheckinState(checkin?: CheckinType): ResolvedCheckinState { + if (!checkin) + return { type: 'NO_CHECKIN' } + + const hasToday = Checkin.hasCheckinToday(checkin.checkin_streak, checkin) + if (hasToday) { + switch (checkin.status) { + case 'WAITING': return { type: 'WAITING' } + case 'APPROVED': return { type: 'APPROVED' } + default: return { type: 'REJECTED' } + } + } + + if (checkin.status === 'APPROVED' && isDateYesterday(checkin.created_at)) + return { type: 'NO_CHECKIN' } + + return { type: 'LAST_CHECKIN' } + } + static async getEmbedStatusContent(guild: Guild, userDiscordId: string, checkin?: CheckinType) { let content = '' let embed: EmbedBuilder diff --git a/src/types/checkin.d.ts b/src/types/checkin.d.ts index 521ef8d..dde80b8 100644 --- a/src/types/checkin.d.ts +++ b/src/types/checkin.d.ts @@ -28,3 +28,9 @@ export interface CheckinColumn { key: T value: Prisma.CheckinWhereInput[T] | Prisma.CheckinWhereUniqueInput[K] } + +export type ResolvedCheckinState = | { type: 'NO_CHECKIN' } + | { type: 'WAITING' } + | { type: 'APPROVED' } + | { type: 'REJECTED' } + | { type: 'LAST_CHECKIN' } From fda1085368fa59a37c4c7097fe0326c42c112d84 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 3 Jan 2026 21:40:33 +0700 Subject: [PATCH 2/6] chore: variable naming --- .../checkin/messages/checkin-status.ts | 20 +++++++++---------- .../checkin/messages/index.ts | 12 +++++------ .../checkin/validators/index.ts | 16 +++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/bot/commands/checkin/messages/checkin-status.ts b/src/bot/commands/checkin/messages/checkin-status.ts index 80c7c8d..3fc8686 100644 --- a/src/bot/commands/checkin/messages/checkin-status.ts +++ b/src/bot/commands/checkin/messages/checkin-status.ts @@ -48,7 +48,7 @@ ${checkin.public_id} πŸ”Ž **Status**: Menunggu peninjauan <@&${FLAMEWARDEN_ROLE}> > *"Percikan telah Tuan/Nona <@${userDiscordId}> titipkan. Mohon menanti sesaat, <@&${FLAMEWARDEN_ROLE}> tengah menakar apakah [nyala tersebut](${checkin.link}) layak menjadi bagian dari perjalanan Tuan/Nona."* `, - ApprovedCheckin: (userDiscordId: string, flamewarden: GuildMember, checkin: Checkin) => ` + ApprovedCheckin: (userDiscordId: string, reviewer: GuildMember, checkin: Checkin) => ` πŸ†” **Check-In ID**: \`\`\`bash ${checkin.public_id} @@ -58,11 +58,11 @@ ${checkin.public_id} πŸ”₯ **Current Streak**: ${checkin.checkin_streak!.streak} day(s) πŸ”Ž **Status**: Disetujui; api Tuan/Nona kian terang πŸ—“ **Approved At**: ${getParsedNow(getNow(checkin.updated_at!))} -πŸ‘€ **Approved By**: <@${flamewarden.id}> -✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'} +πŸ‘€ **Approved By**: <@${reviewer.id}> +✍🏻 **${reviewer.displayName}'(s) Comment**: ${checkin.comment ?? '-'} > *"[Nyala hari ini](${checkin.link}) diterima. Teruslah menenun aksara disiplin, satu hari demi satu hari."* `, - RejectedCheckin: (userDiscordId: string, flamewarden: GuildMember, checkin: Checkin) => ` + RejectedCheckin: (userDiscordId: string, reviewer: GuildMember, checkin: Checkin) => ` πŸ†” **Check-In ID**: \`\`\`bash ${checkin.public_id} @@ -72,11 +72,11 @@ ${checkin.public_id} πŸ”₯ **Current Streak**: ${checkin.checkin_streak!.streak} day(s) πŸ”Ž **Status**: Ditolak; percikan tak cukup kuat πŸ—“ **Reviewed At**: ${getParsedNow(getNow(checkin.updated_at!))} -πŸ‘€ **Reviewed By**: <@${flamewarden.id}> -✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'} +πŸ‘€ **Reviewed By**: <@${reviewer.id}> +✍🏻 **${reviewer.displayName}'(s) Comment**: ${checkin.comment ?? '-'} > *"[Api Tuan/Nona](${checkin.link}) <@${userDiscordId}> meredup hari ini, namun belum padam sepenuhnya. Perbaiki, dan nyalakan kembali percikan yang benar."* `, - LastCheckin: (guildName: string, userDiscordId: string, checkin: Checkin, flamewarden?: GuildMember) => ` + LastCheckin: (guildName: string, userDiscordId: string, checkin: Checkin, reviewer?: GuildMember | null) => ` Wahai Tuan/Nona <@${userDiscordId}>, tercatat bahwa rangkaian nyala api Tuan/Nona telah terputus pada pergantian hari sebelumnya. Namun demikian, percikan terakhir masih tersimpan dalam arsip ${guildName} dan dapat ditinjau kembali. @@ -92,10 +92,10 @@ ${checkin.public_id} πŸ”₯ **Last Streak**: ${checkin.checkin_streak!.streak} day(s) πŸ’₯ **Broken Streak**: ${checkin.checkin_streak!.streak_broken_at ? 'βœ…' : '❌'} πŸ”Ž **Status**: ${checkin.status} -${flamewarden?.displayName +${reviewer?.displayName ? `πŸ—“ **Reviewed At**: ${getParsedNow(getNow(checkin.updated_at!))} -πŸ‘€ **Reviewed By**: ${flamewarden.displayName} (@${flamewarden.user.username}) -✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'}` +πŸ‘€ **Reviewed By**: ${reviewer.displayName} (@${reviewer.user.username}) +✍🏻 **${reviewer.displayName}'(s) Comment**: ${checkin.comment ?? '-'}` : ''} > *"[Percikan ini](${checkin.link}) pernah kamu titipkan pada api, namun belum sempat ditakar oleh penjaga nyala."* `, diff --git a/src/bot/events/interaction-create/checkin/messages/index.ts b/src/bot/events/interaction-create/checkin/messages/index.ts index 521753b..c995a69 100644 --- a/src/bot/events/interaction-create/checkin/messages/index.ts +++ b/src/bot/events/interaction-create/checkin/messages/index.ts @@ -47,7 +47,7 @@ ${checkin.public_id} > πŸ”Ž Sedang menunggu peninjauan Flamewarden; mohon Tuan/Nona bersabar`, - CheckinApproved: (flamewarden: GuildMember, checkin: Checkin) => ` + CheckinApproved: (reviewer: GuildMember, checkin: Checkin) => ` [Nyala api](${checkin.link}) Tuan/Nona berkobar lebih terang pada hari ini. πŸ†” **Check-In ID**: \`\`\`bash @@ -55,12 +55,12 @@ ${checkin.public_id} \`\`\` πŸ”₯ **Current Streak**: ${checkin.checkin_streak!.streak} πŸ—“ **Approved At**: ${getParsedNow(getNow(checkin.updated_at!))} -πŸ‘€ **Approved By**: <@${flamewarden.id}> -✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'} +πŸ‘€ **Approved By**: <@${reviewer.id}> +✍🏻 **${reviewer.displayName}'(s) Comment**: ${checkin.comment ?? '-'} > πŸ”₯ Konsistensi ialah bahan bakar nyala api; teruskan langkah Tuan/Nona`, - CheckinRejected: (flamewarden: GuildMember, checkin: Checkin) => ` + CheckinRejected: (reviewer: GuildMember, checkin: Checkin) => ` [Check-in ini](${checkin.link}) tidak memenuhi syarat dan dengan demikian telah ditolak. πŸ†” **Check-In ID**: \`\`\`bash @@ -68,8 +68,8 @@ ${checkin.public_id} \`\`\` πŸ”₯ **Current Streak**: ${checkin.checkin_streak!.streak} πŸ—“ **Reviewed At**: ${getParsedNow(getNow(checkin.updated_at!))} -πŸ‘€ **Reviewed By**: <@${flamewarden.id}> -✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'} +πŸ‘€ **Reviewed By**: <@${reviewer.id}> +✍🏻 **${reviewer.displayName}'(s) Comment**: ${checkin.comment ?? '-'} > 🧯 Nyala api Tuan/Nona meredup, namun belum padam; silakan mencuba kembali`, } diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 27b6884..301f7f7 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -421,7 +421,7 @@ export class Checkin extends CheckinMessage { static async validateCheckin( client: Client, guild: Guild, - flamewarden: GuildMember, + reviewer: GuildMember, opt: CheckinColumn, checkinCreatedAt: Date, checkinStatus: CheckinStatusType, @@ -430,19 +430,19 @@ export class Checkin extends CheckinMessage { ): Promise { if (!isAudit) await this.assertSubmittedCheckinToday(client.prisma, opt) - const updatedCheckin = await this.updateCheckinStatus(client.prisma, flamewarden, opt, checkinCreatedAt, checkinStatus, comment, isAudit) as CheckinType + const updatedCheckin = await this.updateCheckinStatus(client.prisma, reviewer, opt, checkinCreatedAt, checkinStatus, comment, isAudit) as CheckinType const checkinChannel = await getChannel(guild, CHECKIN_CHANNEL) as TextChannel const { messageId } = this.getMessageFromLink(updatedCheckin.link!) const message = await checkinChannel.messages.fetch(messageId) - await this.validateCheckinHandleToUser(guild, flamewarden, updatedCheckin.user!.discord_id, updatedCheckin) + await this.validateCheckinHandleToUser(guild, reviewer, updatedCheckin.user!.discord_id, updatedCheckin) await this.editSubmittedCheckinMessage(message, checkinStatus) return updatedCheckin } - static async validateCheckinHandleToUser(guild: Guild, flamewarden: GuildMember, userDiscordId: string, updatedCheckin: CheckinType) { + static async validateCheckinHandleToUser(guild: Guild, reviewer: GuildMember, userDiscordId: string, updatedCheckin: CheckinType) { const member = await getMember(guild, userDiscordId) this.assertMember(member) @@ -452,7 +452,7 @@ export class Checkin extends CheckinMessage { const newGrindRole = this.getNewGrindRole(guild, updatedCheckin.checkin_streak!.streak) await this.setMemberNewGrindRole(guild, member, newGrindRole) - await this.sendCheckinStatusToMember(flamewarden, member, updatedCheckin) + await this.sendCheckinStatusToMember(reviewer, member, updatedCheckin) } static async editSubmittedCheckinMessage(message: Message, checkinStatus: CheckinStatusType) { @@ -532,14 +532,14 @@ export class Checkin extends CheckinMessage { await member.send({ embeds: [embed] }) } - static async sendCheckinStatusToMember(flamewarden: GuildMember, member: GuildMember, checkin: CheckinType) { + static async sendCheckinStatusToMember(reviewer: GuildMember, member: GuildMember, checkin: CheckinType) { let embed: EmbedBuilder switch (checkin.status) { case 'REJECTED': embed = createEmbed( `⚠️ *Check-In* Ditolak`, - this.MSG.CheckinRejected(flamewarden, checkin), + this.MSG.CheckinRejected(reviewer, checkin), '#D9534F', { text: DUMMY.FOOTER(member.guild.name) }, ) @@ -548,7 +548,7 @@ export class Checkin extends CheckinMessage { case 'APPROVED': embed = createEmbed( `πŸ”₯ *Check-In* Disetujui`, - this.MSG.CheckinApproved(flamewarden, checkin), + this.MSG.CheckinApproved(reviewer, checkin), '#4CAF50', { text: DUMMY.FOOTER(member.guild.name) }, ) From 7c0f662db13d20c39f7c575906120e4e3de4ad0b Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 3 Jan 2026 21:41:23 +0700 Subject: [PATCH 3/6] refactor: `getEmbedStatusContent` func --- .../checkin/handlers/checkin-status.ts | 5 +- .../checkin/validators/checkin-status.ts | 112 +++++++++--------- src/types/checkin.d.ts | 14 ++- src/utils/discord/index.ts | 2 +- 4 files changed, 67 insertions(+), 66 deletions(-) diff --git a/src/bot/commands/checkin/handlers/checkin-status.ts b/src/bot/commands/checkin/handlers/checkin-status.ts index 881327e..a341716 100644 --- a/src/bot/commands/checkin/handlers/checkin-status.ts +++ b/src/bot/commands/checkin/handlers/checkin-status.ts @@ -1,7 +1,7 @@ import type { ChatInputCommandInteraction, Client, GuildMember, TextChannel } from 'discord.js' import { registerCommand } from '@commands/registry' import { FLAMEWARDEN_ROLE } from '@config/discord' -import { getBot, sendReply } from '@utils/discord' +import { getBot, getMember, sendReply } from '@utils/discord' import { DiscordBaseError } from '@utils/discord/error' import { log } from '@utils/logger' import { SlashCommandBuilder } from 'discord.js' @@ -33,10 +33,13 @@ registerCommand({ const userDiscordId: string = interaction.user.id const user = await CheckinStatus.getUser(client.prisma, userDiscordId) + const checkin = user?.checkins?.[0] + const reviewer = checkin?.reviewed_by ? await getMember(interaction.guild, checkin.reviewed_by) : null const { content, embed } = await CheckinStatus.getEmbedStatusContent( interaction.guild, user?.discord_id ?? member.id, user?.checkins?.[0], + reviewer, ) await sendReply(interaction, content, false, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } }) diff --git a/src/bot/commands/checkin/validators/checkin-status.ts b/src/bot/commands/checkin/validators/checkin-status.ts index 8caf1c6..25b3a82 100644 --- a/src/bot/commands/checkin/validators/checkin-status.ts +++ b/src/bot/commands/checkin/validators/checkin-status.ts @@ -1,12 +1,12 @@ import type { PrismaClient } from '@generatedDB/client' -import type { CheckinStatusType, Checkin as CheckinType, ResolvedCheckinState } from '@type/checkin' +import type { CheckinStatusEmbedContent, Checkin as CheckinType, ResolvedCheckinState } from '@type/checkin' import type { User } from '@type/user' -import type { EmbedBuilder, Guild, Interaction, ThreadAutoArchiveDuration } from 'discord.js' +import type { Guild, GuildMember, Interaction, ThreadAutoArchiveDuration } from 'discord.js' import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' import { Checkin } from '@events/interaction-create/checkin/validators' import { createEmbed, decodeSnowflakes } from '@utils/component' import { isDateYesterday } from '@utils/date' -import { DiscordAssert, getMember } from '@utils/discord' +import { DiscordAssert } from '@utils/discord' import { DUMMY } from '@utils/placeholder' import { messageLink, PermissionsBitField } from 'discord.js' import { CheckinStatusError } from '../handlers/checkin-status' @@ -56,73 +56,67 @@ export class CheckinStatus extends CheckinStatusMessage { return { type: 'LAST_CHECKIN' } } - static async getEmbedStatusContent(guild: Guild, userDiscordId: string, checkin?: CheckinType) { - let content = '' - let embed: EmbedBuilder - - const checkinStreak = checkin?.checkin_streak - const hasCheckedInToday = Checkin.hasCheckinToday(checkinStreak, checkin) - - if (checkin && hasCheckedInToday) { - const flamewarden = await getMember(guild, checkin.reviewed_by!) - - switch (checkin.status as CheckinStatusType) { - case 'WAITING': { - content = `<@&${FLAMEWARDEN_ROLE}>` - embed = createEmbed( - `🧭 Check-In #${checkin.public_id}`, - CheckinStatus.MSG.WaitingCheckin(userDiscordId, checkin), + static async getEmbedStatusContent(guild: Guild, userDiscordId: string, checkin?: CheckinType, reviewer?: GuildMember | null): Promise { + const state = this.resolveCheckinState(checkin) + const footer = { text: DUMMY.FOOTER(guild.name) } + + switch (state.type) { + case 'WAITING': + return { + content: `<@&${FLAMEWARDEN_ROLE}>`, + embed: createEmbed( + `🧭 Check-In #${checkin!.public_id}`, + this.MSG.WaitingCheckin(userDiscordId, checkin!), DUMMY.COLOR, - { text: DUMMY.FOOTER(guild.name) }, - ) - break + footer, + ), } - case 'APPROVED': { - embed = createEmbed( - `πŸ”₯ Check-In #${checkin.public_id}`, - CheckinStatus.MSG.ApprovedCheckin(userDiscordId, flamewarden, checkin), + case 'APPROVED': + return { + embed: createEmbed( + `πŸ”₯ Check-In #${checkin!.public_id}`, + this.MSG.ApprovedCheckin(userDiscordId, reviewer!, checkin!), DUMMY.COLOR, - { text: DUMMY.FOOTER(guild.name) }, - ) - break + footer, + ), } - default: { - embed = createEmbed( - `❌ Check-In #${checkin.public_id}`, - CheckinStatus.MSG.RejectedCheckin(userDiscordId, flamewarden, checkin), + case 'REJECTED': + return { + embed: createEmbed( + `❌ Check-In #${checkin!.public_id}`, + this.MSG.RejectedCheckin(userDiscordId, reviewer!, checkin!), DUMMY.COLOR, - { text: DUMMY.FOOTER(guild.name) }, - ) - break + footer, + ), } - } - - return { content, embed } - } - const shouldShowNoCheckin = !checkin || (checkin.status === 'APPROVED' && isDateYesterday(checkin.created_at)) - if (shouldShowNoCheckin) { - embed = createEmbed( - `🧐 Check-In`, - CheckinStatus.MSG.NoCheckin(userDiscordId, checkinStreak), - DUMMY.COLOR, - { text: DUMMY.FOOTER(guild.name) }, - ) + case 'LAST_CHECKIN': + return { + embed: createEmbed( + `πŸ•―οΈ Check-In #${checkin!.public_id}`, + this.MSG.LastCheckin( + guild.name, + userDiscordId, + checkin!, + reviewer, + ), + DUMMY.COLOR, + footer, + ), + } - return { content, embed } + case 'NO_CHECKIN': + return { + embed: createEmbed( + `🧐 Check-In`, + this.MSG.NoCheckin(userDiscordId, checkin?.checkin_streak), + DUMMY.COLOR, + footer, + ), + } } - - const flamewarden = checkin.reviewed_by ? await getMember(guild, checkin.reviewed_by) : undefined - embed = createEmbed( - `πŸ•―οΈ Check-In #${checkin.public_id}`, - CheckinStatus.MSG.LastCheckin(guild.name, userDiscordId, checkin, flamewarden), - DUMMY.COLOR, - { text: DUMMY.FOOTER(guild.name) }, - ) - - return { content, embed } } static async getUser(prisma: PrismaClient, userDiscordId: string): Promise { diff --git a/src/types/checkin.d.ts b/src/types/checkin.d.ts index dde80b8..e348611 100644 --- a/src/types/checkin.d.ts +++ b/src/types/checkin.d.ts @@ -5,6 +5,11 @@ import type { User } from './user' export type CheckinStatusType = 'WAITING' | 'APPROVED' | 'REJECTED' export type CheckinAllowedEmojiType = '❌' | 'πŸ”₯' +export type ResolvedCheckinState = | { type: 'NO_CHECKIN' } + | { type: 'WAITING' } + | { type: 'APPROVED' } + | { type: 'REJECTED' } + | { type: 'LAST_CHECKIN' } export interface Checkin { id: number @@ -29,8 +34,7 @@ export interface CheckinColumn { value: Prisma.CheckinWhereInput[T] | Prisma.CheckinWhereUniqueInput[K] } -export type ResolvedCheckinState = | { type: 'NO_CHECKIN' } - | { type: 'WAITING' } - | { type: 'APPROVED' } - | { type: 'REJECTED' } - | { type: 'LAST_CHECKIN' } +export interface CheckinStatusEmbedContent { + content?: string + embed: EmbedBuilder +} diff --git a/src/utils/discord/index.ts b/src/utils/discord/index.ts index 3dbddcc..c031285 100644 --- a/src/utils/discord/index.ts +++ b/src/utils/discord/index.ts @@ -50,7 +50,7 @@ export function getAttachments(interaction: ChatInputCommandInteraction, fileCou export async function sendReply( interaction: Interaction, - content: string, + content?: string, ephemeral = true, payloads?: InteractionReplyOptions, isDeferred = false, From f746fd0918290019f2d480cf0395256cd790c70d Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 3 Jan 2026 21:51:37 +0700 Subject: [PATCH 4/6] feat: add member validation on reset grinder roles --- .../jobs/validators/reset-grinder-roles.ts | 13 ++++++------- src/utils/discord/message.ts | 2 +- 2 files changed, 7 insertions(+), 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 6a4fcd5..a5c42eb 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 @@ -71,20 +71,16 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { static async validateWaitingCheckin(guild: Guild, auditFlameChannel: TextChannel, member: GuildMember, user: User, checkin: CheckinType): Promise { if (checkin && checkin.status as CheckinStatusType === 'WAITING') { - const { content, embed } = await CheckinStatus.getEmbedStatusContent( - guild, - user.discord_id, - checkin, - ) + const { content, embed } = await CheckinStatus.getEmbedStatusContent(guild, user.discord_id, checkin) const message = await sendAsBot(null, auditFlameChannel, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] }, content }) as Message + await message.react(CheckinStatus.CLARIFICATION_EMOJI) + const thread = await message.startThread({ name: CheckinStatus.MSG.ThreadName(checkin.public_id), reason: CheckinStatus.MSG.ThreadReason(member.user.tag), autoArchiveDuration: CheckinStatus.THREAD_ARCHIVE_DURATION, }) - await thread.send({ content: CheckinStatus.MSG.ThreadContent(user.discord_id, checkin) }) - await message.react(CheckinStatus.CLARIFICATION_EMOJI) return thread } @@ -104,6 +100,9 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { continue const member = members.get(user.discord_id) as GuildMember + if (!member) + continue + await this.removeGrinderRoles(member) await this.breakCheckinStreakAt(prisma, checkinStreak, lastCheckin!) const thread = await this.validateWaitingCheckin(guild, auditFlameChannel, member, user, lastCheckin!) diff --git a/src/utils/discord/message.ts b/src/utils/discord/message.ts index 57643a6..6825931 100644 --- a/src/utils/discord/message.ts +++ b/src/utils/discord/message.ts @@ -3,7 +3,7 @@ import { formatList } from '@utils/text' export class DiscordMessage { static readonly ERR = { - NoMember: '❌ Couldn’t resolve your member record', + NoMember: '❌ Couldn’t resolve the member', NotGuild: '❌ This action must be used in a server', ChannelNotFound: '❌ Channel not found', RoleUneditable: '❌ I can’t manage that role (check role hierarchy/managed role/@everyone)', From 9ecd69919c7392e7bc1b8e126c894094949a7489 Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 3 Jan 2026 21:52:42 +0700 Subject: [PATCH 5/6] chore: variable naming --- .../client-ready/jobs/validators/reset-grinder-roles.ts | 4 ++-- 1 file changed, 2 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 a5c42eb..6496e6a 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 @@ -69,7 +69,7 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { } } - static async validateWaitingCheckin(guild: Guild, auditFlameChannel: TextChannel, member: GuildMember, user: User, checkin: CheckinType): Promise { + static async notifyWaitingCheckin(guild: Guild, auditFlameChannel: TextChannel, member: GuildMember, user: User, checkin: CheckinType): Promise { if (checkin && checkin.status as CheckinStatusType === 'WAITING') { const { content, embed } = await CheckinStatus.getEmbedStatusContent(guild, user.discord_id, checkin) const message = await sendAsBot(null, auditFlameChannel, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] }, content }) as Message @@ -105,7 +105,7 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { await this.removeGrinderRoles(member) await this.breakCheckinStreakAt(prisma, checkinStreak, lastCheckin!) - const thread = await this.validateWaitingCheckin(guild, auditFlameChannel, member, user, lastCheckin!) + const thread = await this.notifyWaitingCheckin(guild, auditFlameChannel, member, user, lastCheckin!) const payloads: InteractionReplyOptions = { content: ResetGrinderRoles.MSG.GoodBye(guild.name, member), From 29d87ee959d67057c4680d61867fe7ad4a60703f Mon Sep 17 00:00:00 2001 From: alfianchii Date: Sat, 3 Jan 2026 21:57:23 +0700 Subject: [PATCH 6/6] refactor: `validateUsers` with `processUser` func --- .../jobs/validators/reset-grinder-roles.ts | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 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 6496e6a..55df2a9 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 @@ -92,10 +92,11 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { for (const user of users) { const checkinStreak = user.checkin_streaks?.[0] + const lastCheckin = checkinStreak?.checkins?.[0] + if (!checkinStreak) continue - const lastCheckin = checkinStreak.checkins?.[0] if (this.hasValidCheckin(lastCheckin)) continue @@ -103,25 +104,47 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { if (!member) continue - await this.removeGrinderRoles(member) - await this.breakCheckinStreakAt(prisma, checkinStreak, lastCheckin!) - const thread = await this.notifyWaitingCheckin(guild, auditFlameChannel, member, user, lastCheckin!) - - const payloads: InteractionReplyOptions = { - content: ResetGrinderRoles.MSG.GoodBye(guild.name, member), - allowedMentions: { users: [member.id], roles: [] }, - } - if (thread) - payloads.components = [this.generateButton(guild.id, thread)] - - await sendAsBot( - null, + await this.processUser( + prisma, + guild, + member, + user, + checkinStreak, + lastCheckin!, grindAshesChannel, - payloads, + auditFlameChannel, ) + } + } - log.info(this.MSG.RemoveGrinderRoleFrom(member)) + static async processUser( + prisma: PrismaClient, + guild: Guild, + member: GuildMember, + user: User, + checkinStreak: CheckinStreak, + lastCheckin: CheckinType, + grindAshesChannel: TextChannel, + auditFlameChannel: TextChannel, + ) { + await this.removeGrinderRoles(member) + await this.breakCheckinStreakAt(prisma, checkinStreak, lastCheckin!) + const thread = await this.notifyWaitingCheckin(guild, auditFlameChannel, member, user, lastCheckin!) + + const payloads: InteractionReplyOptions = { + content: ResetGrinderRoles.MSG.GoodBye(guild.name, member), + allowedMentions: { users: [member.id], roles: [] }, } + if (thread) + payloads.components = [this.generateButton(guild.id, thread)] + + await sendAsBot( + null, + grindAshesChannel, + payloads, + ) + + log.info(this.MSG.RemoveGrinderRoleFrom(member)) } static async getUsersWithLatestStreak(prisma: PrismaClient): Promise {