diff --git a/src/bot/commands/checkin/handlers/checkin-audit.ts b/src/bot/commands/checkin/handlers/checkin-audit.ts index 1e322d7..f21ceed 100644 --- a/src/bot/commands/checkin/handlers/checkin-audit.ts +++ b/src/bot/commands/checkin/handlers/checkin-audit.ts @@ -18,12 +18,7 @@ export class CheckinAuditError extends DiscordBaseError { registerCommand({ data: new SlashCommandBuilder() .setName('checkin-audit') - .setDescription('Review an old check-in using its public ID.') - .addStringOption(opt => - opt.setName('checkin-id') - .setDescription('Check-In ID (e.g., CHK-A1B2C3)') - .setRequired(true), - ), + .setDescription('Review an old check-in using its public ID.'), async execute(client: Client, interaction: ChatInputCommandInteraction) { try { @@ -41,7 +36,7 @@ registerCommand({ CheckinAudit.assertMember(flamewarden) CheckinAudit.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE) - const checkinId = interaction.options.getString('checkin-id', true) + const checkinId = CheckinAudit.assertCheckinIdFromThread(thread, threadMsg) const checkin = await CheckinAudit.assertExistCheckinId(client.prisma, checkinId) CheckinAudit.assertClarificationThread(thread, checkin.public_id) CheckinAudit.assertCheckinNotToday(checkin) diff --git a/src/bot/commands/checkin/handlers/checkin-status.ts b/src/bot/commands/checkin/handlers/checkin-status.ts index fcccc87..0ea5e03 100644 --- a/src/bot/commands/checkin/handlers/checkin-status.ts +++ b/src/bot/commands/checkin/handlers/checkin-status.ts @@ -1,4 +1,4 @@ -import type { ChatInputCommandInteraction, Client, GuildMember, InteractionReplyOptions } from 'discord.js' +import type { ChatInputCommandInteraction, Client, GuildMember } from 'discord.js' import { registerCommand } from '@commands/registry' import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord' import { sendReply } from '@utils/discord' @@ -31,17 +31,13 @@ registerCommand({ const userDiscordId: string = interaction.user.id const user = await CheckinStatus.getUser(client.prisma, userDiscordId) - const { content, embed, buttons } = await CheckinStatus.getEmbedStatusContent( + const { content, embed } = await CheckinStatus.getEmbedStatusContent( interaction.guild, user?.discord_id ?? member.id, user?.checkins?.[0], ) - const payloads = { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } } as InteractionReplyOptions - if (buttons) - payloads.components = [buttons] - - await sendReply(interaction, content, false, payloads) + await sendReply(interaction, content, false, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } }) } catch (err: any) { if (err instanceof DiscordBaseError) diff --git a/src/bot/commands/checkin/messages/checkin-status.ts b/src/bot/commands/checkin/messages/checkin-status.ts index d2f010c..80c7c8d 100644 --- a/src/bot/commands/checkin/messages/checkin-status.ts +++ b/src/bot/commands/checkin/messages/checkin-status.ts @@ -1,7 +1,7 @@ import type { Checkin } from '@type/checkin' import type { CheckinStreak } from '@type/checkin-streak' import type { GuildMember, PublicThreadChannel } from 'discord.js' -import { FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord' +import { FLAMEWARDEN_ROLE } from '@config/discord' import { getNow, getParsedNow } from '@utils/date' import { DiscordAssert } from '@utils/discord' @@ -15,8 +15,8 @@ export class CheckinStatusMessage extends DiscordAssert { ...DiscordAssert.MSG, ThreadName: (publicId: string) => `❓ Klarifikasi Check-In #${publicId}`, ThreadReason: (userTag: string) => `Check-in clarification requested by ${userTag}`, - ThreadContent: (checkin: Checkin) => ` -πŸ‘€ <@${checkin.user!.discord_id}> meminta klarifikasi untuk [*check-in*](${checkin.link!}) ini. + ThreadContent: (discordId: string, checkin: Checkin) => ` +πŸ‘€ <@${discordId}> meminta klarifikasi untuk [*check-in*](${checkin.link!}) ini. πŸ”₯ <@&${FLAMEWARDEN_ROLE}> mohon ditinjau. Teristimewa untuk <@&${FLAMEWARDEN_ROLE}>, silakan gunakan *command* **\`/checkin-audit\`** untuk melakukan *review* terhadap *check-in*. @@ -97,18 +97,7 @@ ${flamewarden?.displayName πŸ‘€ **Reviewed By**: ${flamewarden.displayName} (@${flamewarden.user.username}) ✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'}` : ''} -> *"[Percikan ini](${checkin.link}) pernah kau titipkan pada api, namun belum sempat ditakar oleh penjaga nyala."* - `, - LastCheckinNote: (guildName: string, checkinLink: string, statusLink: string) => ` -Apabila Tuan/Nona meyakini bahwa [*check-in*](${checkinLink}) belum sempat ditinjau oleh <@&${FLAMEWARDEN_ROLE}>, -maka ${guildName} membuka ruang klarifikasi dengan tata cara sebagai berikut: -β… . Berikan reaksi ❓ pada pesan [*status check-in*](${statusLink}) ini. -β…‘. Sebuah *thread* khusus akan tercipta secara otomatis. -β…’. Gunakan *thread* tersebut untuk berkomunikasi dan mengajukan peninjauan kepada <@&${FLAMEWARDEN_ROLE}>. - -⚠️ Ketentuan Penting: -Selama proses klarifikasi berlangsung, Tuan/Nona tidak diperkenankan terlebih dahulu memasuki <#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan. -Waktu klarifikasi dibuka maksimal 1x24 jam sejak *check-in* diajukan. +> *"[Percikan ini](${checkin.link}) pernah kamu titipkan pada api, namun belum sempat ditakar oleh penjaga nyala."* `, } } diff --git a/src/bot/commands/checkin/validators/checkin-status.ts b/src/bot/commands/checkin/validators/checkin-status.ts index 616dd01..2a0aae5 100644 --- a/src/bot/commands/checkin/validators/checkin-status.ts +++ b/src/bot/commands/checkin/validators/checkin-status.ts @@ -3,14 +3,12 @@ import type { CheckinStatusType, Checkin as CheckinType } 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' -import { CHECKIN_STATUS_CLARIFICATION_BUTTON_ID } from '@events/interaction-create/checkin/handlers/status-clarification-button' -import { CHECKIN_STATUS_NOTE_BUTTON_ID } from '@events/interaction-create/checkin/handlers/status-note-button' import { Checkin } from '@events/interaction-create/checkin/validators' -import { createEmbed, decodeSnowflakes, encodeSnowflake, getCustomId } from '@utils/component' +import { createEmbed, decodeSnowflakes } from '@utils/component' import { isDateYesterday } from '@utils/date' import { DiscordAssert } from '@utils/discord' import { DUMMY } from '@utils/placeholder' -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, messageLink, PermissionsBitField } from 'discord.js' +import { messageLink, PermissionsBitField } from 'discord.js' import { CheckinStatusError } from '../handlers/checkin-status' import { CheckinStatusMessage } from '../messages/checkin-status' @@ -98,7 +96,6 @@ export class CheckinStatus extends CheckinStatusMessage { } const flamewarden = await guild.members.fetch(checkin.reviewed_by!) - const buttons = this.generateButtons(guild.id, checkin) embed = createEmbed( `πŸ•―οΈ Check-In #${checkin.public_id}`, CheckinStatus.MSG.LastCheckin(guild.name, userDiscordId, checkin, flamewarden), @@ -106,27 +103,7 @@ export class CheckinStatus extends CheckinStatusMessage { { text: DUMMY.FOOTER(guild.name) }, ) - return { content, embed, buttons } - } - - static generateButtons(guildId: string, checkin: CheckinType): ActionRowBuilder | undefined { - if (checkin.status === 'WAITING') { - const { messageId } = this.getMessageFromLink(checkin.link!) - - const noteButtonId = getCustomId([CHECKIN_STATUS_NOTE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)]) - const noteButton = new ButtonBuilder() - .setCustomId(noteButtonId) - .setLabel('πŸ“œ Maklumat Klarifikasi') - .setStyle(ButtonStyle.Primary) - - const clarificationButtonId = getCustomId([CHECKIN_STATUS_CLARIFICATION_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)]) - const clarificationButton = new ButtonBuilder() - .setCustomId(clarificationButtonId) - .setLabel('❓ Ajukan Klarifikasi') - .setStyle(ButtonStyle.Success) - - return new ActionRowBuilder().addComponents(noteButton, clarificationButton) - } + return { content, embed } } static async getUser(prisma: PrismaClient, userDiscordId: string): Promise { 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 7ff831e..cadb4b2 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,6 +1,6 @@ import type { Client, TextChannel } from 'discord.js' import process from 'node:process' -import { GRIND_ASHES_CHANNEL } from '@config/discord' +import { AUDIT_FLAME_CHANNEL, GRIND_ASHES_CHANNEL } from '@config/discord' import { registerClientReadyHandler } from '@events/client-ready/registry' import { EVENT_PATH } from '@events/index' import { getChannel } from '@utils/discord' @@ -27,11 +27,13 @@ registerClientReadyHandler({ log.check(ResetGrinderRoles.MSG.JobRunning) const guild = await client.guilds.fetch(process.env.GUILD_ID!) - const channel = await getChannel(guild, GRIND_ASHES_CHANNEL) as TextChannel - ResetGrinderRoles.assertChannel(channel) + const grindAshesChannel = await getChannel(guild, GRIND_ASHES_CHANNEL) as TextChannel + ResetGrinderRoles.assertChannel(grindAshesChannel) + const auditFlameChannel = await getChannel(guild, AUDIT_FLAME_CHANNEL) as TextChannel + ResetGrinderRoles.assertChannel(auditFlameChannel) const users = await ResetGrinderRoles.getUsersWithLatestStreak(client.prisma) - await ResetGrinderRoles.validateUsers(client.prisma, guild, channel, users) + await ResetGrinderRoles.validateUsers(client.prisma, guild, grindAshesChannel, auditFlameChannel, users) log.success(ResetGrinderRoles.MSG.JobSuccess) }) 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 77d4ae0..776963b 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,4 +1,4 @@ -import type { GuildMember } from 'discord.js' +import type { GuildMember, ThreadChannel } from 'discord.js' import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord' import { DiscordAssert } from '@utils/discord' @@ -23,13 +23,13 @@ Namun jangan berduka, jalan ini selalu terbuka bagi mereka yang bersedia memulai *${guildName} menanti mereka yang konsisten.* `, - GoodByeNotes: ` + GoodByeNotes: (thread: ThreadChannel) => ` > 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: > β… . Jangan terlebih dahulu memasuki ⁠<#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan. -> β…‘. Silakan menjalankan perintah **\`/checkin-status\`** pada <#${AUDIT_FLAME_CHANNEL}> untuk menampilkan status *check-in* terakhir Tuan/Nona. -> β…’. Setelah pesan status tersebut muncul, berikan reaksi "❓" pada pesan tersebut. -> β…£. 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. +> β…‘. Pada saat pergantian hari (pukul 00:00 WIB), sistem akan secara otomatis menampilkan arsip *check-in* terakhir Tuan/Nona di kanal <#${AUDIT_FLAME_CHANNEL}>, lengkap dengan penanda bahwa rangkaian nyala telah terputus. +> β…’. Bersamaan dengan pesan tersebut, sebuah [*thread* klarifikasi](${thread.url}) akan tercipta secara otomatis, sebagai ruang resmi untuk peninjauan, penandaan, dan komunikasi antara Tuan/Nona dengan <@&${FLAMEWARDEN_ROLE}>. +> β…£. Tuan/Nona dipersilakan menanti proses audit di dalam *thread* tersebut. Apabila diperlukan, Tuan/Nona dapat menyampaikan penjelasan tambahan atau melakukan penandaan dengan tertib, tanpa membuka *check-in* baru terlebih dahulu. +> ⏳ Waktu peninjauan dan klarifikasi dibuka maksimal 1Γ—24 jam sejak pesan arsip *check-in* tersebut ditampilkan. `, } } 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 0d341da..2b23647 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,12 +1,14 @@ import type { PrismaClient } from '@generatedDB/client' +import type { CheckinStatusType, Checkin as CheckinType } from '@type/checkin' import type { CheckinStreak } from '@type/checkin-streak' import type { User } from '@type/user' -import type { Guild, GuildMember, Interaction, TextChannel } from 'discord.js' -import { getGrindRoles, GRINDER_ROLE } from '@config/discord' +import type { Guild, GuildMember, Interaction, InteractionReplyOptions, Message, PublicThreadChannel, TextChannel, ThreadChannel } from 'discord.js' +import { CheckinStatus } from '@commands/checkin/validators/checkin-status' +import { FLAMEWARDEN_ROLE, getGrindRoles, GRINDER_ROLE } from '@config/discord' 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 { DiscordAssert, sendAsBot } from '@utils/discord' +import { DiscordAssert, getChannel, sendAsBot } from '@utils/discord' import { log } from '@utils/logger' import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js' import { ResetGrinderRolesMessage } from '../messages/reset-grinder-roles' @@ -16,19 +18,25 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { ...DiscordAssert.BASE_PERMS, ] - static getButtonId(interaction: Interaction, customId: string) { - const [prefix, guildId] = decodeSnowflakes(customId) + static async getButtonId(interaction: Interaction, customId: string) { + const [prefix, guildId, threadId] = decodeSnowflakes(customId) if (!guildId) throw new ResetGrinderRolesButtonError(this.ERR.GuildMissing) if (interaction.guildId !== guildId) throw new ResetGrinderRolesButtonError(this.ERR.NotGuild) + if (!threadId) + throw new ResetGrinderRolesButtonError(this.ERR.ThreadIdMissing) - return { prefix, guildId } + const thread = await getChannel(interaction.guild!, threadId, true) as ThreadChannel + if (!thread) + throw new ResetGrinderRolesButtonError(this.ERR.ThreadNotFound) + + return { prefix, guildId, thread } } - static generateButton(guildId: string): ActionRowBuilder { - const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId)]) + static generateButton(guildId: string, thread: ThreadChannel): ActionRowBuilder { + const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(thread.id)]) const noteButton = new ButtonBuilder() .setCustomId(noteButtonId) .setLabel('πŸ“œ Ketentuan Peninjauan Api') @@ -61,7 +69,28 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { } } - static async validateUsers(prisma: PrismaClient, guild: Guild, channel: TextChannel, users: User[]) { + 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 message = await sendAsBot(null, auditFlameChannel, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] }, content }) as Message + 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 + } + } + + static async validateUsers(prisma: PrismaClient, guild: Guild, grindAshesChannel: TextChannel, auditFlameChannel: TextChannel, users: User[]) { for (const user of users) { const checkinStreak = user.checkin_streaks?.[0] if (!checkinStreak) @@ -73,13 +102,20 @@ 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 this.breakCheckinStreakAt(prisma, checkinStreak, lastCheckin!) + const thread = await this.validateWaitingCheckin(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, - channel, - { content: ResetGrinderRoles.MSG.GoodBye(guild.name, member), components: [button], allowedMentions: { users: [member.id], roles: [] } }, + grindAshesChannel, + payloads, ) log.info(this.MSG.RemoveGrinderRoleFrom(member)) @@ -100,6 +136,7 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { checkins: { orderBy: { created_at: 'desc' }, take: 1, + include: { checkin_streak: true }, }, }, }, @@ -109,13 +146,15 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage { return users } - static async breakCheckinStreakAt(prisma: PrismaClient, checkinStreak: CheckinStreak) { - await prisma.checkinStreak.update({ + static async breakCheckinStreakAt(prisma: PrismaClient, checkinStreak: CheckinStreak, checkin: CheckinType) { + const streak = await prisma.checkinStreak.update({ where: { id: checkinStreak.id }, data: { streak_broken_at: new Date(), updated_at: new Date(), }, - }) + }) as CheckinStreak + + checkin.checkin_streak = streak } } diff --git a/src/bot/events/interaction-create/checkin/handlers/status-clarification-button.ts b/src/bot/events/interaction-create/checkin/handlers/status-clarification-button.ts deleted file mode 100644 index 4aef35c..0000000 --- a/src/bot/events/interaction-create/checkin/handlers/status-clarification-button.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { CheckinStatusType } from '@type/checkin' -import type { TextChannel } from 'discord.js' -import { CheckinStatus } from '@commands/checkin/validators/checkin-status' -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 { getModuleName } from '@utils/io' -import { Checkin } from '../validators' - -export class CheckinStatusClarificationButtonError extends DiscordBaseError { - constructor(message: string, options?: { cause?: unknown }) { - super('CheckinStatusClarificationButtonError', message, options) - } -} - -const moduleName = getModuleName(EVENT_PATH, __filename) -export const CHECKIN_STATUS_CLARIFICATION_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` - -registerInteractionHandler({ - desc: 'Creates a thread for the grinder to discuss check-in clarification with Flamewarden when the clarification button is clicked.', - id: CHECKIN_STATUS_CLARIFICATION_BUTTON_ID, - errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedButton}`, - async exec(client, interaction) { - if (!interaction.isButton()) - return - - try { - if (!interaction.inCachedGuild()) - throw new CheckinStatusClarificationButtonError(Checkin.ERR.NotGuild) - - const { checkinLink } = CheckinStatus.getButtonId(interaction, interaction.customId) - - const channel = interaction.channel as TextChannel - Checkin.assertMissPerms(interaction.client.user, channel) - - const checkin = await Checkin.getWaitingCheckin(client.prisma, 'link', checkinLink) - Checkin.assertWaitingCheckin(checkin.status as CheckinStatusType, checkin.link!) - Checkin.assertOwnedCheckin(checkin.user!.discord_id, interaction.user.id) - CheckinStatus.assertHasThread(interaction.message) - - const thread = await interaction.message.startThread({ - name: CheckinStatus.MSG.ThreadName(checkin.public_id), - reason: CheckinStatus.MSG.ThreadReason(interaction.user.tag), - autoArchiveDuration: CheckinStatus.THREAD_ARCHIVE_DURATION, - }) - - await thread.send({ content: CheckinStatus.MSG.ThreadContent(checkin) }) - await sendReply(interaction, CheckinStatus.MSG.ThreadCreated(thread)) - await interaction.message.react(CheckinStatus.CLARIFICATION_EMOJI) - } - catch (err: any) { - if (err instanceof DiscordBaseError) - await sendReply(interaction, err.message) - else throw err - } - }, -}) diff --git a/src/bot/events/interaction-create/checkin/handlers/status-note-button.ts b/src/bot/events/interaction-create/checkin/handlers/status-note-button.ts deleted file mode 100644 index 6072a95..0000000 --- a/src/bot/events/interaction-create/checkin/handlers/status-note-button.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { TextChannel } from 'discord.js' -import { CheckinStatus } from '@commands/checkin/validators/checkin-status' -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 { getModuleName } from '@utils/io' -import { messageLink } from 'discord.js' -import { Checkin } from '../validators' - -export class CheckinStatusNoteButtonError extends DiscordBaseError { - constructor(message: string, options?: { cause?: unknown }) { - super('CheckinStatusNoteButtonError', message, options) - } -} - -const moduleName = getModuleName(EVENT_PATH, __filename) -export const CHECKIN_STATUS_NOTE_BUTTON_ID = `${generateCustomId(EVENT_PATH, __filename)}` - -registerInteractionHandler({ - desc: 'Opens a note about how to request clarification for the last check-in if the streak was broken and did not reviewed.', - id: CHECKIN_STATUS_NOTE_BUTTON_ID, - errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedButton}`, - async exec(_, interaction) { - if (!interaction.isButton()) - return - - try { - if (!interaction.inCachedGuild()) - throw new CheckinStatusNoteButtonError(Checkin.ERR.NotGuild) - - const { checkinLink } = CheckinStatus.getButtonId(interaction, interaction.customId) - - const channel = interaction.channel as TextChannel - Checkin.assertMissPerms(interaction.client.user, channel) - - const statusMessageLink = messageLink(interaction.channelId, interaction.message.id, interaction.guildId) - - await sendReply(interaction, CheckinStatus.MSG.LastCheckinNote(interaction.guild.name, checkinLink, statusMessageLink)) - } - catch (err: any) { - if (err instanceof DiscordBaseError) - await sendReply(interaction, err.message) - else throw err - } - }, -}) diff --git a/src/bot/events/interaction-create/checkin/messages/audit.ts b/src/bot/events/interaction-create/checkin/messages/audit.ts index 1ea0197..2d72bb3 100644 --- a/src/bot/events/interaction-create/checkin/messages/audit.ts +++ b/src/bot/events/interaction-create/checkin/messages/audit.ts @@ -11,6 +11,11 @@ export class CheckinAuditMessage extends DiscordAssert { ${waitingCheckinList} `, NotClarificationThread: '❌ This thread does not correspond to the correct check-in. Please make sure you are reviewing the correct clarification thread', + NoOldestCheckins: '❌ There are no waiting check-ins to audit for this user', + ThreadMessageMissingEmbed: '❌ The thread message is missing the expected embed. Please ensure the clarification thread contains an embed', + ThreadMessageMissingTitle: '❌ The thread message embed is missing a title. Please ensure the embed contains a check-in ID in its title', + ThreadOrEmbedMissingId: '❌ Could not find the check-in ID in the thread name or in the embed title', + ThreadIdEmbedIdMismatch: '❌ The check-in ID in the thread name does not match the embed title. Please verify that you are reviewing the correct thread', UnexpectedCheckinAudit: '❌ Something went wrong during the check-in audit', } diff --git a/src/bot/events/interaction-create/checkin/validators/audit.ts b/src/bot/events/interaction-create/checkin/validators/audit.ts index efd2367..cb13d8b 100644 --- a/src/bot/events/interaction-create/checkin/validators/audit.ts +++ b/src/bot/events/interaction-create/checkin/validators/audit.ts @@ -53,6 +53,9 @@ export class CheckinAudit extends CheckinAuditMessage { } static assertCheckinWithOldestWaiting(currCheckin: CheckinType, checkins: CheckinType[]) { + if (!checkins.length) + throw new CheckinAuditError(CheckinAudit.ERR.NoOldestCheckins) + const oldestWaitingCheckin = checkins[0] const diffMs = Math.abs(currCheckin.created_at.getTime() - oldestWaitingCheckin.created_at.getTime()) @@ -80,6 +83,30 @@ ${checkin.public_id} } } + static assertCheckinIdFromThread(thread: ThreadChannel, threadMsg: Message): string { + const threadName = thread.name + const embeds = threadMsg.embeds + if (!embeds || !embeds.length) + throw new CheckinAuditError(this.ERR.ThreadMessageMissingEmbed) + + const embedTitle = embeds[0].title + if (!embedTitle) + throw new CheckinAuditError(this.ERR.ThreadMessageMissingTitle) + + const idRegex = Checkin.getCheckinIdRegex() + const nameMatch = threadName.match(idRegex) + const titleMatch = embedTitle.match(idRegex) + if (!nameMatch || !titleMatch) + throw new CheckinAuditError(this.ERR.ThreadOrEmbedMissingId) + + const threadId = nameMatch[0] + const embedId = titleMatch[0] + if (threadId !== embedId) + throw new CheckinAuditError(this.ERR.ThreadIdEmbedIdMismatch) + + return threadId + } + static async assertExistCheckinId(prisma: PrismaClient, checkinId: string) { const checkin = await prisma.checkin.findUnique({ where: { public_id: checkinId }, diff --git a/src/bot/events/interaction-create/checkin/validators/index.ts b/src/bot/events/interaction-create/checkin/validators/index.ts index 72c4fc9..bff1464 100644 --- a/src/bot/events/interaction-create/checkin/validators/index.ts +++ b/src/bot/events/interaction-create/checkin/validators/index.ts @@ -42,6 +42,10 @@ export class Checkin extends CheckinMessage { Object.entries(this.EMOJI_STATUS).map(([emoji, status]) => [status, emoji]), ) as Record + static getCheckinIdRegex() { + return new RegExp(`${this.PUBLIC_ID_PREFIX}[A-Z0-9]+`, 'i') + } + static getModalId(interaction: Interaction, customId: string) { const [prefix, guildId, tempToken] = decodeSnowflakes(customId) @@ -208,18 +212,6 @@ export class Checkin extends CheckinMessage { return emoji as CheckinAllowedEmojiType } - static assertWaitingCheckin(checkinStatus: CheckinStatusType, checkinMsgLink: string) { - if (checkinStatus !== 'WAITING') { - throw new SubmittedCheckinError(this.ERR.NotWaitingCheckin(checkinMsgLink)) - } - } - - static assertOwnedCheckin(checkinUserDiscordId: string, currentUserId: string) { - if (checkinUserDiscordId !== currentUserId) { - throw new SubmittedCheckinError(this.ERR.NotYourCheckin) - } - } - static async getOrCreateUser(prisma: PrismaClient, userDiscordId: string): Promise { const select = { id: true, 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 5bfa45c..0bdd59c 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 @@ -31,7 +31,9 @@ registerInteractionHandler({ const channel = interaction.channel as TextChannel ResetGrinderRoles.assertMissPerms(interaction.client.user, channel) - await sendReply(interaction, ResetGrinderRoles.MSG.GoodByeNotes) + const { thread } = await ResetGrinderRoles.getButtonId(interaction, interaction.customId) + + await sendReply(interaction, ResetGrinderRoles.MSG.GoodByeNotes(thread)) } catch (err: any) { if (err instanceof DiscordBaseError) diff --git a/src/utils/discord/index.ts b/src/utils/discord/index.ts index a393b61..d58c8a0 100644 --- a/src/utils/discord/index.ts +++ b/src/utils/discord/index.ts @@ -94,6 +94,8 @@ export async function sendAsBot( const msg = await channel.send(opts) if (isTempMessage) setTimeout(() => msg?.delete().catch(() => {}), 5000) + + return msg } export * from './assert' diff --git a/src/utils/discord/message.ts b/src/utils/discord/message.ts index 943123a..57643a6 100644 --- a/src/utils/discord/message.ts +++ b/src/utils/discord/message.ts @@ -21,6 +21,8 @@ export class DiscordMessage { CannotPost: '❌ I can’t post in that channel', MessageIdMissing: '❌ Message ID is missing or invalid', MessageLinkInvalid: '❌ The provided message link is invalid', + ThreadIdMissing: '❌ Thread ID is missing or invalid', + ThreadNotFound: '❌ The specified thread could not be found', ChannelAlreadyHasThread: '❌ This channel message already has an associated thread', MustBeThread: (parentChannelId: string) => `❌ This action can only be performed in a thread under <#${parentChannelId}>`, ArchivedThread: '❌ This thread is archived', diff --git a/src/utils/discord/roles.ts b/src/utils/discord/roles.ts index 1dd8c75..e02a277 100644 --- a/src/utils/discord/roles.ts +++ b/src/utils/discord/roles.ts @@ -2,9 +2,19 @@ import type { GrindRole } from '@config/discord' import type { GuildMember, RoleManager } from 'discord.js' import { getGrindRoles } from '@config/discord' -export function getGrindRoleByStreakCount(roleManager: RoleManager, streak_count: number) { - const role = getGrindRoles(roleManager).find(role => streak_count === role.threshold) - return role +export function getGrindRoleByStreakCount(roles: RoleManager, streakCount: number) { + let mactchedRole: GrindRole | undefined + + for (const role of getGrindRoles(roles)) { + if (streakCount >= role.threshold) { + mactchedRole = role + } + else { + break + } + } + + return mactchedRole } export async function attachNewGrindRole(member: GuildMember, grindRole: GrindRole) {