Skip to content

Commit 199bc73

Browse files
authored
Fix/checkin audit pt3 (#56)
* fix: logic of get grind role by streak * chore: checkin clarification workflow instructions * fix: remove buttons on last waiting checkin * refactor: waiting checkin clarification and its thread created on 00:00 instead * fix: channels on reset grindder roles * refactor: show button only when waiting checkin * feat: linked thread msg on goodbye notes * chore: typography * feat: assertion for oldest checkin * feat: thread checkin id err messages * feat: `/checkin-audit` without param, use thread title instead * fix: updated streak, then make thread
1 parent c0a20bd commit 199bc73

File tree

16 files changed

+135
-204
lines changed

16 files changed

+135
-204
lines changed

src/bot/commands/checkin/handlers/checkin-audit.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@ export class CheckinAuditError extends DiscordBaseError {
1818
registerCommand({
1919
data: new SlashCommandBuilder()
2020
.setName('checkin-audit')
21-
.setDescription('Review an old check-in using its public ID.')
22-
.addStringOption(opt =>
23-
opt.setName('checkin-id')
24-
.setDescription('Check-In ID (e.g., CHK-A1B2C3)')
25-
.setRequired(true),
26-
),
21+
.setDescription('Review an old check-in using its public ID.'),
2722

2823
async execute(client: Client, interaction: ChatInputCommandInteraction) {
2924
try {
@@ -41,7 +36,7 @@ registerCommand({
4136
CheckinAudit.assertMember(flamewarden)
4237
CheckinAudit.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE)
4338

44-
const checkinId = interaction.options.getString('checkin-id', true)
39+
const checkinId = CheckinAudit.assertCheckinIdFromThread(thread, threadMsg)
4540
const checkin = await CheckinAudit.assertExistCheckinId(client.prisma, checkinId)
4641
CheckinAudit.assertClarificationThread(thread, checkin.public_id)
4742
CheckinAudit.assertCheckinNotToday(checkin)

src/bot/commands/checkin/handlers/checkin-status.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ChatInputCommandInteraction, Client, GuildMember, InteractionReplyOptions } from 'discord.js'
1+
import type { ChatInputCommandInteraction, Client, GuildMember } from 'discord.js'
22
import { registerCommand } from '@commands/registry'
33
import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord'
44
import { sendReply } from '@utils/discord'
@@ -31,17 +31,13 @@ registerCommand({
3131
const userDiscordId: string = interaction.user.id
3232
const user = await CheckinStatus.getUser(client.prisma, userDiscordId)
3333

34-
const { content, embed, buttons } = await CheckinStatus.getEmbedStatusContent(
34+
const { content, embed } = await CheckinStatus.getEmbedStatusContent(
3535
interaction.guild,
3636
user?.discord_id ?? member.id,
3737
user?.checkins?.[0],
3838
)
3939

40-
const payloads = { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } } as InteractionReplyOptions
41-
if (buttons)
42-
payloads.components = [buttons]
43-
44-
await sendReply(interaction, content, false, payloads)
40+
await sendReply(interaction, content, false, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } })
4541
}
4642
catch (err: any) {
4743
if (err instanceof DiscordBaseError)

src/bot/commands/checkin/messages/checkin-status.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Checkin } from '@type/checkin'
22
import type { CheckinStreak } from '@type/checkin-streak'
33
import type { GuildMember, PublicThreadChannel } from 'discord.js'
4-
import { FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord'
4+
import { FLAMEWARDEN_ROLE } from '@config/discord'
55
import { getNow, getParsedNow } from '@utils/date'
66
import { DiscordAssert } from '@utils/discord'
77

@@ -15,8 +15,8 @@ export class CheckinStatusMessage extends DiscordAssert {
1515
...DiscordAssert.MSG,
1616
ThreadName: (publicId: string) => `❓ Klarifikasi Check-In #${publicId}`,
1717
ThreadReason: (userTag: string) => `Check-in clarification requested by ${userTag}`,
18-
ThreadContent: (checkin: Checkin) => `
19-
👤 <@${checkin.user!.discord_id}> meminta klarifikasi untuk [*check-in*](${checkin.link!}) ini.
18+
ThreadContent: (discordId: string, checkin: Checkin) => `
19+
👤 <@${discordId}> meminta klarifikasi untuk [*check-in*](${checkin.link!}) ini.
2020
🔥 <@&${FLAMEWARDEN_ROLE}> mohon ditinjau.
2121
2222
Teristimewa untuk <@&${FLAMEWARDEN_ROLE}>, silakan gunakan *command* **\`/checkin-audit\`** untuk melakukan *review* terhadap *check-in*.
@@ -97,18 +97,7 @@ ${flamewarden?.displayName
9797
👀 **Reviewed By**: ${flamewarden.displayName} (@${flamewarden.user.username})
9898
✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'}`
9999
: ''}
100-
> *"[Percikan ini](${checkin.link}) pernah kau titipkan pada api, namun belum sempat ditakar oleh penjaga nyala."*
101-
`,
102-
LastCheckinNote: (guildName: string, checkinLink: string, statusLink: string) => `
103-
Apabila Tuan/Nona meyakini bahwa [*check-in*](${checkinLink}) belum sempat ditinjau oleh <@&${FLAMEWARDEN_ROLE}>,
104-
maka ${guildName} membuka ruang klarifikasi dengan tata cara sebagai berikut:
105-
Ⅰ. Berikan reaksi ❓ pada pesan [*status check-in*](${statusLink}) ini.
106-
Ⅱ. Sebuah *thread* khusus akan tercipta secara otomatis.
107-
Ⅲ. Gunakan *thread* tersebut untuk berkomunikasi dan mengajukan peninjauan kepada <@&${FLAMEWARDEN_ROLE}>.
108-
109-
⚠️ Ketentuan Penting:
110-
Selama proses klarifikasi berlangsung, Tuan/Nona tidak diperkenankan terlebih dahulu memasuki <#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan.
111-
Waktu klarifikasi dibuka maksimal 1x24 jam sejak *check-in* diajukan.
100+
> *"[Percikan ini](${checkin.link}) pernah kamu titipkan pada api, namun belum sempat ditakar oleh penjaga nyala."*
112101
`,
113102
}
114103
}

src/bot/commands/checkin/validators/checkin-status.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import type { CheckinStatusType, Checkin as CheckinType } from '@type/checkin'
33
import type { User } from '@type/user'
44
import type { EmbedBuilder, Guild, Interaction, ThreadAutoArchiveDuration } from 'discord.js'
55
import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord'
6-
import { CHECKIN_STATUS_CLARIFICATION_BUTTON_ID } from '@events/interaction-create/checkin/handlers/status-clarification-button'
7-
import { CHECKIN_STATUS_NOTE_BUTTON_ID } from '@events/interaction-create/checkin/handlers/status-note-button'
86
import { Checkin } from '@events/interaction-create/checkin/validators'
9-
import { createEmbed, decodeSnowflakes, encodeSnowflake, getCustomId } from '@utils/component'
7+
import { createEmbed, decodeSnowflakes } from '@utils/component'
108
import { isDateYesterday } from '@utils/date'
119
import { DiscordAssert } from '@utils/discord'
1210
import { DUMMY } from '@utils/placeholder'
13-
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, messageLink, PermissionsBitField } from 'discord.js'
11+
import { messageLink, PermissionsBitField } from 'discord.js'
1412
import { CheckinStatusError } from '../handlers/checkin-status'
1513
import { CheckinStatusMessage } from '../messages/checkin-status'
1614

@@ -98,35 +96,14 @@ export class CheckinStatus extends CheckinStatusMessage {
9896
}
9997

10098
const flamewarden = await guild.members.fetch(checkin.reviewed_by!)
101-
const buttons = this.generateButtons(guild.id, checkin)
10299
embed = createEmbed(
103100
`🕯️ Check-In #${checkin.public_id}`,
104101
CheckinStatus.MSG.LastCheckin(guild.name, userDiscordId, checkin, flamewarden),
105102
DUMMY.COLOR,
106103
{ text: DUMMY.FOOTER(guild.name) },
107104
)
108105

109-
return { content, embed, buttons }
110-
}
111-
112-
static generateButtons(guildId: string, checkin: CheckinType): ActionRowBuilder<ButtonBuilder> | undefined {
113-
if (checkin.status === 'WAITING') {
114-
const { messageId } = this.getMessageFromLink(checkin.link!)
115-
116-
const noteButtonId = getCustomId([CHECKIN_STATUS_NOTE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)])
117-
const noteButton = new ButtonBuilder()
118-
.setCustomId(noteButtonId)
119-
.setLabel('📜 Maklumat Klarifikasi')
120-
.setStyle(ButtonStyle.Primary)
121-
122-
const clarificationButtonId = getCustomId([CHECKIN_STATUS_CLARIFICATION_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)])
123-
const clarificationButton = new ButtonBuilder()
124-
.setCustomId(clarificationButtonId)
125-
.setLabel('❓ Ajukan Klarifikasi')
126-
.setStyle(ButtonStyle.Success)
127-
128-
return new ActionRowBuilder<ButtonBuilder>().addComponents(noteButton, clarificationButton)
129-
}
106+
return { content, embed }
130107
}
131108

132109
static async getUser(prisma: PrismaClient, userDiscordId: string): Promise<User> {

src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Client, TextChannel } from 'discord.js'
22
import process from 'node:process'
3-
import { GRIND_ASHES_CHANNEL } from '@config/discord'
3+
import { AUDIT_FLAME_CHANNEL, GRIND_ASHES_CHANNEL } from '@config/discord'
44
import { registerClientReadyHandler } from '@events/client-ready/registry'
55
import { EVENT_PATH } from '@events/index'
66
import { getChannel } from '@utils/discord'
@@ -27,11 +27,13 @@ registerClientReadyHandler({
2727
log.check(ResetGrinderRoles.MSG.JobRunning)
2828

2929
const guild = await client.guilds.fetch(process.env.GUILD_ID!)
30-
const channel = await getChannel(guild, GRIND_ASHES_CHANNEL) as TextChannel
31-
ResetGrinderRoles.assertChannel(channel)
30+
const grindAshesChannel = await getChannel(guild, GRIND_ASHES_CHANNEL) as TextChannel
31+
ResetGrinderRoles.assertChannel(grindAshesChannel)
32+
const auditFlameChannel = await getChannel(guild, AUDIT_FLAME_CHANNEL) as TextChannel
33+
ResetGrinderRoles.assertChannel(auditFlameChannel)
3234
const users = await ResetGrinderRoles.getUsersWithLatestStreak(client.prisma)
3335

34-
await ResetGrinderRoles.validateUsers(client.prisma, guild, channel, users)
36+
await ResetGrinderRoles.validateUsers(client.prisma, guild, grindAshesChannel, auditFlameChannel, users)
3537

3638
log.success(ResetGrinderRoles.MSG.JobSuccess)
3739
})

src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GuildMember } from 'discord.js'
1+
import type { GuildMember, ThreadChannel } from 'discord.js'
22
import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord'
33
import { DiscordAssert } from '@utils/discord'
44

@@ -23,13 +23,13 @@ Namun jangan berduka, jalan ini selalu terbuka bagi mereka yang bersedia memulai
2323
2424
*${guildName} menanti mereka yang konsisten.*
2525
`,
26-
GoodByeNotes: `
26+
GoodByeNotes: (thread: ThreadChannel) => `
2727
> 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:
2828
> Ⅰ. Jangan terlebih dahulu memasuki ⁠<#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan.
29-
> Ⅱ. Silakan menjalankan perintah **\`/checkin-status\`** pada <#${AUDIT_FLAME_CHANNEL}> untuk menampilkan status *check-in* terakhir Tuan/Nona.
30-
> Ⅲ. Setelah pesan status tersebut muncul, berikan reaksi "❓" pada pesan tersebut.
31-
> Ⅳ. Dari reaksi tersebut, sebuah *thread* akan tercipta secara otomatis sebagai ruang klarifikasi dan komunikasi dengan <@&${FLAMEWARDEN_ROLE}>.
32-
> ⏳ Batas waktu penantian atas status *WAITING* adalah maksimal 1×24 jam sejak *check-in* diajukan.
29+
> Ⅱ. 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.
30+
> Ⅲ. 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}>.
31+
> Ⅳ. 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.
32+
> ⏳ Waktu peninjauan dan klarifikasi dibuka maksimal 1×24 jam sejak pesan arsip *check-in* tersebut ditampilkan.
3333
`,
3434
}
3535
}

src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { PrismaClient } from '@generatedDB/client'
2+
import type { CheckinStatusType, Checkin as CheckinType } from '@type/checkin'
23
import type { CheckinStreak } from '@type/checkin-streak'
34
import type { User } from '@type/user'
4-
import type { Guild, GuildMember, Interaction, TextChannel } from 'discord.js'
5-
import { getGrindRoles, GRINDER_ROLE } from '@config/discord'
5+
import type { Guild, GuildMember, Interaction, InteractionReplyOptions, Message, PublicThreadChannel, TextChannel, ThreadChannel } from 'discord.js'
6+
import { CheckinStatus } from '@commands/checkin/validators/checkin-status'
7+
import { FLAMEWARDEN_ROLE, getGrindRoles, GRINDER_ROLE } from '@config/discord'
68
import { GOODBYE_NOTE_BUTTON_ID, ResetGrinderRolesButtonError } from '@events/interaction-create/jobs/handlers/reset-grinder-roles-button'
79
import { decodeSnowflakes, encodeSnowflake, getCustomId } from '@utils/component'
810
import { isDateToday, isDateYesterday } from '@utils/date'
9-
import { DiscordAssert, sendAsBot } from '@utils/discord'
11+
import { DiscordAssert, getChannel, sendAsBot } from '@utils/discord'
1012
import { log } from '@utils/logger'
1113
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'
1214
import { ResetGrinderRolesMessage } from '../messages/reset-grinder-roles'
@@ -16,19 +18,25 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage {
1618
...DiscordAssert.BASE_PERMS,
1719
]
1820

19-
static getButtonId(interaction: Interaction, customId: string) {
20-
const [prefix, guildId] = decodeSnowflakes(customId)
21+
static async getButtonId(interaction: Interaction, customId: string) {
22+
const [prefix, guildId, threadId] = decodeSnowflakes(customId)
2123

2224
if (!guildId)
2325
throw new ResetGrinderRolesButtonError(this.ERR.GuildMissing)
2426
if (interaction.guildId !== guildId)
2527
throw new ResetGrinderRolesButtonError(this.ERR.NotGuild)
28+
if (!threadId)
29+
throw new ResetGrinderRolesButtonError(this.ERR.ThreadIdMissing)
2630

27-
return { prefix, guildId }
31+
const thread = await getChannel(interaction.guild!, threadId, true) as ThreadChannel
32+
if (!thread)
33+
throw new ResetGrinderRolesButtonError(this.ERR.ThreadNotFound)
34+
35+
return { prefix, guildId, thread }
2836
}
2937

30-
static generateButton(guildId: string): ActionRowBuilder<ButtonBuilder> {
31-
const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId)])
38+
static generateButton(guildId: string, thread: ThreadChannel): ActionRowBuilder<ButtonBuilder> {
39+
const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(thread.id)])
3240
const noteButton = new ButtonBuilder()
3341
.setCustomId(noteButtonId)
3442
.setLabel('📜 Ketentuan Peninjauan Api')
@@ -61,7 +69,28 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage {
6169
}
6270
}
6371

64-
static async validateUsers(prisma: PrismaClient, guild: Guild, channel: TextChannel, users: User[]) {
72+
static async validateWaitingCheckin(guild: Guild, auditFlameChannel: TextChannel, member: GuildMember, user: User, checkin: CheckinType): Promise<PublicThreadChannel | undefined> {
73+
if (checkin && checkin.status as CheckinStatusType === 'WAITING') {
74+
const { content, embed } = await CheckinStatus.getEmbedStatusContent(
75+
guild,
76+
user.discord_id,
77+
checkin,
78+
)
79+
const message = await sendAsBot(null, auditFlameChannel, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] }, content }) as Message
80+
const thread = await message.startThread({
81+
name: CheckinStatus.MSG.ThreadName(checkin.public_id),
82+
reason: CheckinStatus.MSG.ThreadReason(member.user.tag),
83+
autoArchiveDuration: CheckinStatus.THREAD_ARCHIVE_DURATION,
84+
})
85+
86+
await thread.send({ content: CheckinStatus.MSG.ThreadContent(user.discord_id, checkin) })
87+
await message.react(CheckinStatus.CLARIFICATION_EMOJI)
88+
89+
return thread
90+
}
91+
}
92+
93+
static async validateUsers(prisma: PrismaClient, guild: Guild, grindAshesChannel: TextChannel, auditFlameChannel: TextChannel, users: User[]) {
6594
for (const user of users) {
6695
const checkinStreak = user.checkin_streaks?.[0]
6796
if (!checkinStreak)
@@ -73,13 +102,20 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage {
73102

74103
const member = await guild.members.fetch(user.discord_id)
75104
await this.removeGrinderRoles(member)
76-
await this.breakCheckinStreakAt(prisma, checkinStreak)
77-
const button = this.generateButton(guild.id)
105+
await this.breakCheckinStreakAt(prisma, checkinStreak, lastCheckin!)
106+
const thread = await this.validateWaitingCheckin(guild, auditFlameChannel, member, user, lastCheckin!)
107+
108+
const payloads: InteractionReplyOptions = {
109+
content: ResetGrinderRoles.MSG.GoodBye(guild.name, member),
110+
allowedMentions: { users: [member.id], roles: [] },
111+
}
112+
if (thread)
113+
payloads.components = [this.generateButton(guild.id, thread)]
78114

79115
await sendAsBot(
80116
null,
81-
channel,
82-
{ content: ResetGrinderRoles.MSG.GoodBye(guild.name, member), components: [button], allowedMentions: { users: [member.id], roles: [] } },
117+
grindAshesChannel,
118+
payloads,
83119
)
84120

85121
log.info(this.MSG.RemoveGrinderRoleFrom(member))
@@ -100,6 +136,7 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage {
100136
checkins: {
101137
orderBy: { created_at: 'desc' },
102138
take: 1,
139+
include: { checkin_streak: true },
103140
},
104141
},
105142
},
@@ -109,13 +146,15 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage {
109146
return users
110147
}
111148

112-
static async breakCheckinStreakAt(prisma: PrismaClient, checkinStreak: CheckinStreak) {
113-
await prisma.checkinStreak.update({
149+
static async breakCheckinStreakAt(prisma: PrismaClient, checkinStreak: CheckinStreak, checkin: CheckinType) {
150+
const streak = await prisma.checkinStreak.update({
114151
where: { id: checkinStreak.id },
115152
data: {
116153
streak_broken_at: new Date(),
117154
updated_at: new Date(),
118155
},
119-
})
156+
}) as CheckinStreak
157+
158+
checkin.checkin_streak = streak
120159
}
121160
}

0 commit comments

Comments
 (0)