Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions src/bot/commands/checkin/handlers/checkin-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
10 changes: 3 additions & 7 deletions src/bot/commands/checkin/handlers/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 4 additions & 15 deletions src/bot/commands/checkin/messages/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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*.
Expand Down Expand Up @@ -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."*
`,
}
}
29 changes: 3 additions & 26 deletions src/bot/commands/checkin/validators/checkin-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -98,35 +96,14 @@ 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),
DUMMY.COLOR,
{ text: DUMMY.FOOTER(guild.name) },
)

return { content, embed, buttons }
}

static generateButtons(guildId: string, checkin: CheckinType): ActionRowBuilder<ButtonBuilder> | 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<ButtonBuilder>().addComponents(noteButton, clarificationButton)
}
return { content, embed }
}

static async getUser(prisma: PrismaClient, userDiscordId: string): Promise<User> {
Expand Down
10 changes: 6 additions & 4 deletions src/bot/events/client-ready/jobs/handlers/reset-grinder-roles.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
})
Expand Down
12 changes: 6 additions & 6 deletions src/bot/events/client-ready/jobs/messages/reset-grinder-roles.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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.
`,
}
}
71 changes: 55 additions & 16 deletions src/bot/events/client-ready/jobs/validators/reset-grinder-roles.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<ButtonBuilder> {
const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId)])
static generateButton(guildId: string, thread: ThreadChannel): ActionRowBuilder<ButtonBuilder> {
const noteButtonId = getCustomId([GOODBYE_NOTE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(thread.id)])
const noteButton = new ButtonBuilder()
.setCustomId(noteButtonId)
.setLabel('📜 Ketentuan Peninjauan Api')
Expand Down Expand Up @@ -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<PublicThreadChannel | undefined> {
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)
Expand All @@ -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))
Expand All @@ -100,6 +136,7 @@ export class ResetGrinderRoles extends ResetGrinderRolesMessage {
checkins: {
orderBy: { created_at: 'desc' },
take: 1,
include: { checkin_streak: true },
},
},
},
Expand All @@ -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
}
}
Loading