diff --git a/src/bot/events/client-ready/jobs/handlers/notify-waiting-checkin.ts b/src/bot/events/client-ready/jobs/handlers/notify-waiting-checkin.ts new file mode 100644 index 0000000..98c4642 --- /dev/null +++ b/src/bot/events/client-ready/jobs/handlers/notify-waiting-checkin.ts @@ -0,0 +1,46 @@ +import type { Client, TextChannel } from 'discord.js' +import process from 'node:process' +import { WARDEN_DUTY_CHANNEL } from '@config/discord' +import { registerClientReadyHandler } from '@events/client-ready/registry' +import { EVENT_PATH } from '@events/index' +import { getChannel } from '@utils/discord' +import { DiscordBaseError } from '@utils/discord/error' +import { getModuleName } from '@utils/io' +import { log } from '@utils/logger' +import cron from 'node-cron' +import { NotifyWaitingCheckin } from '../validators/notify-waiting-checkin' + +export class NotifyWaitingCheckinError extends DiscordBaseError { + constructor(message: string, options?: { cause?: unknown }) { + super('NotifyWaitingCheckinError', message, options) + } +} + +const moduleName = getModuleName(EVENT_PATH, __filename) + +registerClientReadyHandler({ + desc: 'Notifies Flamewardens if there are users with check-in status still waiting for review at 22:00 (WIB).', + errorTag: () => `${moduleName}: ${NotifyWaitingCheckin.ERR.UnexpectedNotifyWaitingCheckin}`, + async exec(client: Client) { + try { + cron.schedule('0 22 * * *', async () => { + log.check(NotifyWaitingCheckin.MSG.JobRunning) + + const guild = await client.guilds.fetch(process.env.GUILD_ID!) + const wardenDutyChannel = await getChannel(guild, WARDEN_DUTY_CHANNEL) as TextChannel + NotifyWaitingCheckin.assertChannel(wardenDutyChannel) + const checkins = await NotifyWaitingCheckin.getTodayWaitingCheckins(client.prisma) + + await NotifyWaitingCheckin.sendOpening(guild.name, wardenDutyChannel) + await NotifyWaitingCheckin.sendList(checkins, wardenDutyChannel) + await NotifyWaitingCheckin.sendClosing(guild.name, wardenDutyChannel) + + log.success(NotifyWaitingCheckin.MSG.JobSuccess) + }) + } + catch (err) { + if (!(err instanceof DiscordBaseError)) + throw err + } + }, +}) diff --git a/src/bot/events/client-ready/jobs/messages/notify-waiting-checkin.ts b/src/bot/events/client-ready/jobs/messages/notify-waiting-checkin.ts new file mode 100644 index 0000000..19caa9a --- /dev/null +++ b/src/bot/events/client-ready/jobs/messages/notify-waiting-checkin.ts @@ -0,0 +1,43 @@ +import type { Checkin } from '@type/checkin' +import { FLAMEWARDEN_ROLE, GRINDER_ROLE } from '@config/discord' +import { getParsedNow } from '@utils/date' +import { DiscordAssert } from '@utils/discord' + +export class NotifyWaitingCheckinMessage extends DiscordAssert { + static override readonly ERR = { + ...DiscordAssert.ERR, + UnexpectedNotifyWaitingCheckin: '❌ Something went wrong while notifying waiting check-in', + } + + static override readonly MSG = { + ...DiscordAssert.MSG, + JobRunning: '[JOB] Running notify waiting checkin...', + JobSuccess: '[JOB] Notify waiting checkin finished successfully', + Opening: (guildName: string) => ` +Wahai para <@&${FLAMEWARDEN_ROLE}>, +tatkala malam kian mendekat dan waktu hampir beralih hari, ${guildName} mencatat bahwa masih terdapat percikan api yang belum ditakar. +**📜 Laporan Status Api** +Beberapa *check-in* para <@&${GRINDER_ROLE}> masih berada dalam keadaan *WAITING* dan belum memperoleh keputusan hingga saat ini. +**⏳ Waktu Genting** +Apabila nyala tersebut tidak ditinjau sebelum 23:59 WIB, +maka rangkaian api para <@&${GRINDER_ROLE}> terkait berisiko gugur pada pergantian hari. +**⚔️ Tugas Penjagaan** +Demi menjaga keadilan perjalanan dan kesinambungan disiplin, +dimohon para <@&${FLAMEWARDEN_ROLE}> berkenan: +Ⅰ. Meninjau *check-in* yang masih tertunda, +Ⅱ. Menetapkan keputusan dengan bijaksana, +Ⅲ. Atau memberi arahan seperlunya sebelum waktu berganti. + `, + List: (checkin: Checkin) => ` +- 🔥 <@${checkin.user!.discord_id}> pada [${getParsedNow(checkin.created_at)}](${checkin.link}) + `, + Closing: ` +Apabila hingga pergantian hari *check-in* di atas belum ditinjau, maka rangkaian nyala para <@&${GRINDER_ROLE}> terkait berisiko terputus oleh hukum waktu. + +Kami mohon kebijaksanaan dan perhatian para <@&${FLAMEWARDEN_ROLE}>, +agar setiap api dinilai dengan adil sebelum malam berganti. + +> *"Api bukan sekadar menyala; ia dijaga agar tak padam oleh kelalaian."* + `, + } +} diff --git a/src/bot/events/client-ready/jobs/validators/notify-waiting-checkin.ts b/src/bot/events/client-ready/jobs/validators/notify-waiting-checkin.ts new file mode 100644 index 0000000..3229252 --- /dev/null +++ b/src/bot/events/client-ready/jobs/validators/notify-waiting-checkin.ts @@ -0,0 +1,83 @@ +import type { PrismaClient } from '@generatedDB/client' +import type { Checkin as CheckinType } from '@type/checkin' +import type { TextChannel } from 'discord.js' +import { FLAMEWARDEN_ROLE, GRINDER_ROLE } from '@config/discord' +import { createEmbed } from '@utils/component' +import { DiscordAssert, sendAsBot } from '@utils/discord' +import { DUMMY } from '@utils/placeholder' +import { NotifyWaitingCheckinMessage } from '../messages/notify-waiting-checkin' + +export class NotifyWaitingCheckin extends NotifyWaitingCheckinMessage { + static override BASE_PERMS = [ + ...DiscordAssert.BASE_PERMS, + ] + + static async sendOpening(guildName: string, wardenDutyChannel: TextChannel) { + const openingEmbed = createEmbed( + `🔥 Maklumat Penjagaan Nyala`, + this.MSG.Opening(guildName), + DUMMY.COLOR, + null, + null, + null, + null, + false, + ) + await sendAsBot(null, wardenDutyChannel, { + content: `<@&${FLAMEWARDEN_ROLE}>`, + embeds: [openingEmbed], + allowedMentions: { roles: [FLAMEWARDEN_ROLE, GRINDER_ROLE] }, + }) + } + + static async sendList(checkins: CheckinType[], wardenDutyChannel: TextChannel) { + const list: string[] = [] + for (const checkin of checkins) { + list.push(this.MSG.List(checkin)) + } + const listEmbed = createEmbed( + `⏳ Daftar Waiting Check-In`, + list.join('\n'), + DUMMY.COLOR, + null, + null, + null, + null, + false, + ) + await sendAsBot(null, wardenDutyChannel, { + embeds: [listEmbed], + }) + } + + static async sendClosing(guildName: string, wardenDutyChannel: TextChannel) { + const closingEmbed = createEmbed( + '🛡️ Amanat Penjagaan', + this.MSG.Closing, + DUMMY.COLOR, + { text: DUMMY.FOOTER(guildName) }, + ) + await sendAsBot(null, wardenDutyChannel, { + embeds: [closingEmbed], + allowedMentions: { roles: [FLAMEWARDEN_ROLE, GRINDER_ROLE] }, + }) + } + + static async getTodayWaitingCheckins(prisma: PrismaClient): Promise { + const waitingCheckins = await prisma.checkin.findMany({ + where: { + status: 'WAITING', + reviewed_by: null, + created_at: { + gte: new Date(new Date().setHours(0, 0, 0, 0)), + }, + }, + include: { + user: true, + }, + orderBy: { created_at: 'asc' }, + }) as CheckinType[] + + return waitingCheckins + } +}