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
12 changes: 9 additions & 3 deletions src/bot/commands/checkin/handlers/checkin-audit.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ChatInputCommandInteraction, Client } from 'discord.js'
import type { ChatInputCommandInteraction, Client, TextChannel } from 'discord.js'
import { registerCommand } from '@commands/registry'
import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord'
import { CHECKIN_AUDIT_ID } from '@events/interaction-create/checkin/handlers/audit-modal'
import { createCheckinReviewModal, encodeSnowflake, getCustomId } from '@utils/component'
import { sendReply } from '@utils/discord'
import { getChannelOrThread, sendReply } from '@utils/discord'
import { DiscordBaseError } from '@utils/discord/error'
import { log } from '@utils/logger'
import { SlashCommandBuilder } from 'discord.js'
Expand All @@ -30,14 +30,20 @@ registerCommand({
if (!interaction.inCachedGuild())
throw new CheckinAuditError(CheckinAudit.ERR.NotGuild)

const channel = await CheckinAudit.assertAllowedChannel(interaction.guild, interaction.channelId, AUDIT_FLAME_CHANNEL)
const channel = await getChannelOrThread(interaction.guild, AUDIT_FLAME_CHANNEL) as TextChannel
CheckinAudit.assertMissPerms(interaction.client.user, channel)
const thread = await CheckinAudit.assertThreadUnderChannel(interaction.guild, interaction.channelId, channel)
CheckinAudit.assertNotArchivedThread(thread)
CheckinAudit.assertNotPrivateThread(thread)
const threadMsg = await CheckinAudit.getThreadMessage(thread)
CheckinAudit.assertThreadMessageSendBy(threadMsg, interaction.client.user.id)
const flamewarden = await interaction.guild.members.fetch(interaction.member.id)
CheckinAudit.assertMember(flamewarden)
CheckinAudit.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE)

const checkinId = interaction.options.getString('checkin-id', true)
const checkin = await CheckinAudit.assertExistCheckinId(client.prisma, checkinId)
CheckinAudit.assertClarificationThread(thread, checkin.public_id)
CheckinAudit.assertCheckinNotToday(checkin)
const checkins = await CheckinAudit.getOldestWaitingCheckins(client.prisma, checkin.checkin_streak_id)
CheckinAudit.assertCheckinWithOldestWaiting(checkin, checkins)
Expand Down
4 changes: 0 additions & 4 deletions src/bot/commands/checkin/handlers/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { ChatInputCommandInteraction, Client, GuildMember, InteractionReplyOptions } from 'discord.js'
import { COMMAND_PATH } from '@commands/index'
import { registerCommand } from '@commands/registry'
import { AUDIT_FLAME_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord'
import { generateCustomId } from '@utils/component'
import { sendReply } from '@utils/discord'
import { DiscordBaseError } from '@utils/discord/error'
import { log } from '@utils/logger'
Expand All @@ -15,8 +13,6 @@ export class CheckinStatusError extends DiscordBaseError {
}
}

export const STATUS_LAST_CHECKIN_CLARIFICATION_BUTTON_ID = `${generateCustomId(COMMAND_PATH, __filename)}`

registerCommand({
data: new SlashCommandBuilder()
.setName('checkin-status')
Expand Down
26 changes: 21 additions & 5 deletions src/bot/commands/checkin/messages/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Checkin } from '@type/checkin'
import type { CheckinStreak } from '@type/checkin-streak'
import type { GuildMember } from 'discord.js'
import type { GuildMember, PublicThreadChannel } from 'discord.js'
import { FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord'
import { getNow, getParsedNow } from '@utils/date'
import { DiscordAssert } from '@utils/discord'
Expand All @@ -13,9 +13,25 @@ export class CheckinStatusMessage extends DiscordAssert {

static override readonly MSG = {
...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.
🔥 <@&${FLAMEWARDEN_ROLE}> mohon ditinjau.

Teristimewa untuk <@&${FLAMEWARDEN_ROLE}>, silakan gunakan *command* **\`/checkin-audit\`** untuk melakukan *review* terhadap *check-in*.
`,
ThreadCreated: (thread: PublicThreadChannel) => `
✅ Sebuah thread klarifikasi telah dibuat:

**${thread.name}**
🔗 [Lihat Thread](${thread.url})

Silakan gunakan thread ini untuk mendiskusikan detail *check-in* bersama <@&${FLAMEWARDEN_ROLE}>.
`,
NoCheckin: (userDiscordId: string, checkinStreak: CheckinStreak | undefined) => `
Wahai Tuan/Nona <@${userDiscordId}>,
Nyala api Tuan/Nona belum dinyalakan hari ini.
nyala api Tuan/Nona belum dinyalakan hari ini.
🔥 **Current Streak**: ${checkinStreak?.streak ?? 0} day(s)
🔎 **Status**: Belum melakukan *check-in*
> *"Percikan hari ini belum ditorehkan. Lakukan check-in sebelum 23:59 WIB, agar api Tuan/Nona tak meredup."*
Expand All @@ -42,7 +58,7 @@ ${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.displayName} (@${flamewarden.user.username})
👀 **Approved By**: <@${flamewarden.id}>
✍🏻 **${flamewarden.displayName}'(s) Comment**: ${checkin.comment ?? '-'}
> *"[Nyala hari ini](${checkin.link}) diterima. Teruslah menenun aksara disiplin, satu hari demi satu hari."*
`,
Expand All @@ -56,13 +72,13 @@ ${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.displayName} (@${flamewarden.user.username})
👀 **Reviewed By**: <@${flamewarden.id}>
✍🏻 **${flamewarden.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: (userDiscordId: string, checkin: Checkin, flamewarden?: GuildMember) => `
Wahai Tuan/Nona <@${userDiscordId}>,
Tercatat bahwa rangkaian nyala api Tuan/Nona telah terputus pada pergantian hari sebelumnya.
tercatat bahwa rangkaian nyala api Tuan/Nona telah terputus pada pergantian hari sebelumnya.
Namun demikian, percikan terakhir masih tersimpan dalam arsip Aksaria dan dapat ditinjau kembali.

Berikut adalah *check-in* terakhir yang pernah Tuan/Nona torehkan:
Expand Down
19 changes: 12 additions & 7 deletions src/bot/commands/checkin/validators/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { PrismaClient } from '@generatedDB/client'
import type { Checkin as CheckinType } from '@type/checkin'
import type { CheckinStatusType, Checkin as CheckinType } from '@type/checkin'
import type { User } from '@type/user'
import type { EmbedBuilder, Guild, Interaction } from 'discord.js'
import type { EmbedBuilder, Guild, Interaction, ThreadAutoArchiveDuration } from 'discord.js'
import { CHECKIN_CHANNEL, FLAMEWARDEN_ROLE } from '@config/discord'
import { STATUS_LAST_CHECKIN_NOTE_BUTTON_ID } from '@events/interaction-create/checkin/handlers/status-last-checkin-note-button'
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 { isDateYesterday } from '@utils/date'
import { DiscordAssert } from '@utils/discord'
import { DUMMY } from '@utils/placeholder'
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, messageLink, PermissionsBitField } from 'discord.js'
import { CheckinStatusError, STATUS_LAST_CHECKIN_CLARIFICATION_BUTTON_ID } from '../handlers/checkin-status'
import { CheckinStatusError } from '../handlers/checkin-status'
import { CheckinStatusMessage } from '../messages/checkin-status'

export class CheckinStatus extends CheckinStatusMessage {
Expand All @@ -19,6 +20,10 @@ export class CheckinStatus extends CheckinStatusMessage {
PermissionsBitField.Flags.UseApplicationCommands,
]

static CLARIFICATION_EMOJI = '❓'

static override THREAD_ARCHIVE_DURATION: ThreadAutoArchiveDuration = 1440

static getButtonId(interaction: Interaction, customId: string) {
const [prefix, guildId, checkinMessageId] = decodeSnowflakes(customId)

Expand All @@ -44,7 +49,7 @@ export class CheckinStatus extends CheckinStatusMessage {
if (checkin && hasCheckedInToday) {
const flamewarden = await guild.members.fetch(checkin.reviewed_by!)

switch (checkin.status) {
switch (checkin.status as CheckinStatusType) {
case 'WAITING': {
content = `<@&${FLAMEWARDEN_ROLE}>`
embed = createEmbed(
Expand Down Expand Up @@ -108,13 +113,13 @@ export class CheckinStatus extends CheckinStatusMessage {
if (checkin.status === 'WAITING') {
const { messageId } = this.getMessageFromLink(checkin.link!)

const noteButtonId = getCustomId([STATUS_LAST_CHECKIN_NOTE_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)])
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([STATUS_LAST_CHECKIN_CLARIFICATION_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)])
const clarificationButtonId = getCustomId([CHECKIN_STATUS_CLARIFICATION_BUTTON_ID, encodeSnowflake(guildId), encodeSnowflake(messageId)])
const clarificationButton = new ButtonBuilder()
.setCustomId(clarificationButtonId)
.setLabel('❓ Ajukan Klarifikasi')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Client } from 'discord.js'
import type { Client, TextChannel } from 'discord.js'
import process from 'node:process'
import { GRIND_ASHES_CHANNEL } from '@config/discord'
import { registerClientReadyHandler } from '@events/client-ready/registry'
import { EVENT_PATH } from '@events/index'
import { getChannel } from '@utils/discord'
import { getChannelOrThread } from '@utils/discord'
import { DiscordBaseError } from '@utils/discord/error'
import { getModuleName } from '@utils/io'
import { log } from '@utils/logger'
Expand All @@ -27,7 +27,7 @@ registerClientReadyHandler({
log.check(ResetGrinderRoles.MSG.JobRunning)

const guild = await client.guilds.fetch(process.env.GUILD_ID!)
const channel = await getChannel(guild, GRIND_ASHES_CHANNEL)
const channel = await getChannelOrThread(guild, GRIND_ASHES_CHANNEL) as TextChannel
ResetGrinderRoles.assertChannel(channel)
const users = await ResetGrinderRoles.getUsersWithLatestStreak(client.prisma)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { TextChannel } from 'discord.js'
import { GRIND_ASHES_CHANNEL, GRINDER_ROLE } from '@config/discord'
import { registerGuildMemberUpdateHandler } from '@events/guild-member-update/registry'
import { EVENT_PATH } from '@events/index'
import { getChannel, sendAsBot } from '@utils/discord'
import { getChannelOrThread, sendAsBot } from '@utils/discord'
import { DiscordBaseError } from '@utils/discord/error'
import { getModuleName } from '@utils/io'
import { GrinderRole } from '../validators'
Expand All @@ -26,7 +27,7 @@ registerGuildMemberUpdateHandler({
const newHasGrinderRole = GrinderRole.isMemberHasRole(newMember, GRINDER_ROLE)
const oldHasGrinderRole = GrinderRole.isMemberHasRole(oldMember, GRINDER_ROLE)
if (newHasGrinderRole && !oldHasGrinderRole) {
const channel = await getChannel(newMember.guild, GRIND_ASHES_CHANNEL)
const channel = await getChannelOrThread(newMember.guild, GRIND_ASHES_CHANNEL) as TextChannel
GrinderRole.assertChannel(channel)
const button = GrinderRole.generateButton(newMember.guild.id)

Expand Down
19 changes: 14 additions & 5 deletions src/bot/events/interaction-create/checkin/handlers/audit-modal.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { CheckinStatusType } from '@type/checkin'
import type { TextChannel } from 'discord.js'
import type { ThreadChannel } from 'discord.js'
import { FLAMEWARDEN_ROLE } from '@config/discord'
import { EVENT_PATH } from '@events/index'
import { registerInteractionHandler } from '@events/interaction-create/registry'
import { generateCustomId } from '@utils/component'
import { createEmbed, generateCustomId } from '@utils/component'
import { sendReply } from '@utils/discord'
import { DiscordBaseError } from '@utils/discord/error'
import { getModuleName } from '@utils/io'
import { DUMMY } from '@utils/placeholder'
import { Checkin } from '../validators'
import { CheckinAudit } from '../validators/audit'

Expand Down Expand Up @@ -35,8 +36,8 @@ registerInteractionHandler({

const { checkinId, checkinCreatedAt } = CheckinAudit.getModalReviewId(interaction, interaction.customId)

const channel = interaction.channel as TextChannel
CheckinAudit.assertMissPerms(interaction.client.user, channel)
const thread = interaction.channel as ThreadChannel
const threadMsg = await CheckinAudit.getThreadMessage(thread)
const flamewarden = await interaction.guild.members.fetch(interaction.member.id)
CheckinAudit.assertMember(flamewarden)
CheckinAudit.assertMemberHasRole(flamewarden, FLAMEWARDEN_ROLE)
Expand All @@ -55,7 +56,15 @@ registerInteractionHandler({
true,
)

await sendReply(interaction, CheckinAudit.MSG.AuditSuccess(updatedCheckin.link!, updatedCheckin.user!.discord_id))
const embed = createEmbed(
`🔥 Audit Check-In Telah Diselesaikan`,
CheckinAudit.MSG.AuditSuccess(updatedCheckin.link!, flamewarden.id, updatedCheckin.user!.discord_id),
DUMMY.COLOR,
{ text: DUMMY.FOOTER },
)

await sendReply(interaction, '', false, { embeds: [embed], allowedMentions: { users: [updatedCheckin.user!.discord_id] } })
await CheckinAudit.closeClarificationThread(thread, threadMsg)
}
catch (err: any) {
if (err instanceof DiscordBaseError)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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
}
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@ import { getModuleName } from '@utils/io'
import { messageLink } from 'discord.js'
import { Checkin } from '../validators'

export class StatusLastCheckinButtonError extends DiscordBaseError {
export class CheckinStatusNoteButtonError extends DiscordBaseError {
constructor(message: string, options?: { cause?: unknown }) {
super('StatusLastCheckinButtonError', message, options)
super('CheckinStatusNoteButtonError', message, options)
}
}

const moduleName = getModuleName(EVENT_PATH, __filename)
export const STATUS_LAST_CHECKIN_NOTE_BUTTON_ID = `${generateCustomId(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: STATUS_LAST_CHECKIN_NOTE_BUTTON_ID,
id: CHECKIN_STATUS_NOTE_BUTTON_ID,
errorTag: () => `${moduleName}: ${Checkin.ERR.UnexpectedButton}`,
async exec(_, interaction) {
if (!interaction.isButton())
return

try {
if (!interaction.inCachedGuild())
throw new StatusLastCheckinButtonError(Checkin.ERR.NotGuild)
throw new CheckinStatusNoteButtonError(Checkin.ERR.NotGuild)

const { checkinLink } = CheckinStatus.getButtonId(interaction, interaction.customId)

Expand Down
11 changes: 10 additions & 1 deletion src/bot/events/interaction-create/checkin/messages/audit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Checkin } from '@type/checkin'
import { getNow, getParsedNow } from '@utils/date'
import { DiscordAssert } from '@utils/discord'

export class CheckinAuditMessage extends DiscordAssert {
Expand All @@ -9,11 +10,19 @@ export class CheckinAuditMessage extends DiscordAssert {
❌ Check-ins must be within 1 day of each other. Please validate [this check-in](${checkin.link!}) first:
${waitingCheckinList}
`,
NotClarificationThread: '❌ This thread does not correspond to the correct check-in. Please make sure you are reviewing the correct clarification thread',
UnexpectedCheckinAudit: '❌ Something went wrong during the check-in audit',
}

static override readonly MSG = {
...DiscordAssert.MSG,
AuditSuccess: (msgLink: string, userDiscordId: string) => `✅ Successfully [audited check-in](${msgLink}) for <@${userDiscordId}>.`,
AuditSuccess: (checkinLink: string, flamewardenId: string, userDiscordId: string) => `
Wahai Tuan/Nona <@${userDiscordId}>,
[percikan](${checkinLink}) yang Tuan/Nona titipkan telah selesai ditakar dan ditetapkan.
🗓 **Audited At**: ${getParsedNow(getNow())}
👀 **Audited By**: <@${flamewardenId}>

> *"Api telah diuji, dan keputusannya kini tercatat dalam Aksaria."*
`,
}
}
Loading