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
22 changes: 14 additions & 8 deletions src/bot/commands/checkin/handlers/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ChatInputCommandInteraction, Client, GuildMember } from 'discord.js'
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, GRINDER_ROLE } from '@config/discord'
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 @@ -13,6 +15,8 @@ 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 All @@ -25,21 +29,23 @@ registerCommand({

const channel = await CheckinStatus.assertAllowedChannel(interaction.guild, interaction.channelId, AUDIT_FLAME_CHANNEL)
CheckinStatus.assertMissPerms(interaction.client.user, channel)
const member = interaction.member as GuildMember
CheckinStatus.assertMember(member)

const userDiscordId: string = interaction.user.id
const member = interaction.member as GuildMember
const user = await CheckinStatus.getUser(client.prisma, userDiscordId)

CheckinStatus.assertMember(member)
CheckinStatus.assertMemberHasRole(member, GRINDER_ROLE)

const { content, embed } = await CheckinStatus.getEmbedStatusContent(
const { content, embed, buttons } = await CheckinStatus.getEmbedStatusContent(
interaction.guild,
user?.discord_id ?? member.id,
user?.checkins?.[0],
)

await sendReply(interaction, content, false, { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } })
const payloads = { embeds: [embed], allowedMentions: { roles: [FLAMEWARDEN_ROLE] } } as InteractionReplyOptions
if (buttons)
payloads.components = [buttons]

await sendReply(interaction, content, false, payloads)
}
catch (err: any) {
if (err instanceof DiscordBaseError)
Expand Down
39 changes: 36 additions & 3 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 } from 'discord.js'
import { FLAMEWARDEN_ROLE } from '@config/discord'
import { FLAMEWARDEN_ROLE, IGNITE_PATH_CHANNEL } from '@config/discord'
import { getNow, getParsedNow } from '@utils/date'
import { DiscordAssert } from '@utils/discord'

Expand All @@ -16,7 +16,6 @@ export class CheckinStatusMessage extends DiscordAssert {
NoCheckin: (userDiscordId: string, checkinStreak: CheckinStreak | undefined) => `
Wahai Tuan/Nona <@${userDiscordId}>,
Nyala api Tuan/Nona belum dinyalakan hari ini.
🗓 **Date**: ${getParsedNow()}
🔥 **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 Down Expand Up @@ -55,11 +54,45 @@ ${checkin.public_id}
🌟 **Grinder**: <@${userDiscordId}>
📁 **Attachment:** ${checkin.attachments?.length ? '✅' : '❌'}
🔥 **Current Streak**: ${checkin.checkin_streak!.streak} day(s)
🔎 **Status**: Disetujui; api Tuan/Nona kian terang
🔎 **Status**: Ditolak; percikan tak cukup kuat
🗓 **Reviewed At**: ${getParsedNow(getNow(checkin.updated_at!))}
👀 **Reviewed By**: ${flamewarden.displayName} (@${flamewarden.user.username})
✍🏻 **${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.
Namun demikian, percikan terakhir masih tersimpan dalam arsip Aksaria dan dapat ditinjau kembali.

Berikut adalah *check-in* terakhir yang pernah Tuan/Nona torehkan:
🆔 **Check-In ID**:
\`\`\`bash
${checkin.public_id}
\`\`\`
🌟 **Grinder**: <@${userDiscordId}>
📁 **Attachment:** ${checkin.attachments?.length ? '✅' : '❌'}
🗓 **Submitted At**: ${getParsedNow(getNow(checkin.created_at))}
🔥 **Last Streak**: ${checkin.checkin_streak!.streak} day(s)
💥 **Broken Streak**: ${checkin.checkin_streak!.streak_broken_at ? '✅' : '❌'}
🔎 **Status**: ${checkin.status}
${flamewarden?.displayName
? `🗓 **Reviewed At**: ${getParsedNow(getNow(checkin.updated_at!))}
👀 **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: (checkinLink: string, statusLink: string) => `
Apabila Tuan/Nona meyakini bahwa [*check-in*](${checkinLink}) belum sempat ditinjau oleh <@&${FLAMEWARDEN_ROLE}>,
maka Aksaria 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.
`,
}
}
81 changes: 69 additions & 12 deletions src/bot/commands/checkin/validators/checkin-status.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { PrismaClient } from '@generatedDB/client'
import type { Checkin as CheckinType } from '@type/checkin'
import type { User } from '@type/user'
import type { EmbedBuilder, Guild } from 'discord.js'
import { FLAMEWARDEN_ROLE } from '@config/discord'
import type { EmbedBuilder, Guild, Interaction } 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 } from '@events/interaction-create/checkin/validators'
import { createEmbed } from '@utils/component'
import { createEmbed, decodeSnowflakes, encodeSnowflake, getCustomId } from '@utils/component'
import { isDateYesterday } from '@utils/date'
import { DiscordAssert } from '@utils/discord'
import { DUMMY } from '@utils/placeholder'
import { PermissionsBitField } from 'discord.js'
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, messageLink, PermissionsBitField } from 'discord.js'
import { CheckinStatusError, STATUS_LAST_CHECKIN_CLARIFICATION_BUTTON_ID } from '../handlers/checkin-status'
import { CheckinStatusMessage } from '../messages/checkin-status'

export class CheckinStatus extends CheckinStatusMessage {
Expand All @@ -16,17 +19,33 @@ export class CheckinStatus extends CheckinStatusMessage {
PermissionsBitField.Flags.UseApplicationCommands,
]

static async getEmbedStatusContent(guild: Guild, userDiscordId: string, checkin: CheckinType | undefined) {
static getButtonId(interaction: Interaction, customId: string) {
const [prefix, guildId, checkinMessageId] = decodeSnowflakes(customId)

if (!guildId)
throw new CheckinStatusError(this.ERR.GuildMissing)
if (interaction.guildId !== guildId)
throw new CheckinStatusError(this.ERR.NotGuild)
if (!checkinMessageId)
throw new CheckinStatusError(this.ERR.CheckinIdMissing)

const checkinLink = messageLink(CHECKIN_CHANNEL, checkinMessageId, interaction.guildId)

return { prefix, guildId, checkinLink }
}

static async getEmbedStatusContent(guild: Guild, userDiscordId: string, checkin?: CheckinType) {
let content = ''
let embed: EmbedBuilder
const checkinStreak = checkin?.checkin_streak

const checkinStreak = checkin?.checkin_streak
const hasCheckedInToday = Checkin.hasCheckinToday(checkinStreak, checkin)
if (hasCheckedInToday && checkin) {

if (checkin && hasCheckedInToday) {
const flamewarden = await guild.members.fetch(checkin.reviewed_by!)

switch (checkin.status) {
case 'WAITING':
case 'WAITING': {
content = `<@&${FLAMEWARDEN_ROLE}>`
embed = createEmbed(
`🧭 Check-In #${checkin.public_id}`,
Expand All @@ -35,36 +54,74 @@ export class CheckinStatus extends CheckinStatusMessage {
{ text: DUMMY.FOOTER },
)
break
}

case 'APPROVED':
case 'APPROVED': {
embed = createEmbed(
`🔥 Check-In #${checkin.public_id}`,
CheckinStatus.MSG.ApprovedCheckin(userDiscordId, flamewarden, checkin),
DUMMY.COLOR,
{ text: DUMMY.FOOTER },
)
break
}

default:
default: {
embed = createEmbed(
`❌ Check-In #${checkin.public_id}`,
CheckinStatus.MSG.RejectedCheckin(userDiscordId, flamewarden, checkin),
DUMMY.COLOR,
{ text: DUMMY.FOOTER },
)
break
}
}

return { content, embed }
}
else {

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 },
)

return { content, embed }
}

return { content, embed }
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(userDiscordId, checkin, flamewarden),
DUMMY.COLOR,
{ text: DUMMY.FOOTER },
)

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([STATUS_LAST_CHECKIN_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 clarificationButton = new ButtonBuilder()
.setCustomId(clarificationButtonId)
.setLabel('❓ Ajukan Klarifikasi')
.setStyle(ButtonStyle.Success)

return new ActionRowBuilder<ButtonBuilder>().addComponents(noteButton, clarificationButton)
}
}

static async getUser(prisma: PrismaClient, userDiscordId: string): Promise<User> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ Namun jangan berduka, jalan ini selalu terbuka bagi mereka yang bersedia memulai
`,
GoodByeNotes: `
> 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:
> 1. Jangan terlebih dahulu memasuki ⁠<#${IGNITE_PATH_CHANNEL}>, demi menjaga ketertiban alur peninjauan.
> 2. Silakan menjalankan perintah **\`/checkin-status\`** pada <#${AUDIT_FLAME_CHANNEL}> untuk menampilkan status *check-in* terakhir Tuan/Nona.
> 3. Setelah pesan status tersebut muncul, berikan reaksi "❓" pada pesan tersebut.
> 4. Dari reaksi tersebut, sebuah *thread* akan tercipta secara otomatis sebagai ruang klarifikasi dan komunikasi dengan <@&${FLAMEWARDEN_ROLE}>.
> . 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.
`,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 StatusLastCheckinButtonError extends DiscordBaseError {
constructor(message: string, options?: { cause?: unknown }) {
super('StatusLastCheckinButtonError', message, options)
}
}

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

try {
if (!interaction.inCachedGuild())
throw new StatusLastCheckinButtonError(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(checkinLink, statusMessageLink))
}
catch (err: any) {
if (err instanceof DiscordBaseError)
await sendReply(interaction, err.message)
else throw err
}
},
})