import { Client, GatewayIntentBits, TextChannel, REST, Routes } from "discord.js"; import { Config } from "@systems/config"; import { postPoll, polls, lockPoll, updatePollMessage } from "@systems/poll"; import { handleInteraction } from "@handlers/interactions"; import { buildTgCommand } from "@commands/tg"; import { buildTgConfigCommand } from "@commands/tgConfig"; import { TGSlot } from "@src/types"; import { persist } from "@systems/pollPersistence" import { buildTgAdminCommand } from "@commands/tgAdmin"; import { Scheduler } from "@systems/scheduler"; import { Runtime, RuntimeEvents } from "@systems/runtime"; import { Leaderboard } from "@systems/leaderboard"; import { Result } from "@systems/result"; import { Attendance } from "@systems/attendance"; import { DiscordClient } from "@src/discord/client"; import { SleepCheck } from "@systems/sleepCheck"; const TOKEN = process.env.DISCORD_TOKEN!; const CLIENT_ID = process.env.CLIENT_ID!; const GUILD_ID = process.env.GUILD_ID!; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], }); async function registerCommands(): Promise { const rest = new REST({ version: "10" }).setToken(TOKEN); await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: [ buildTgCommand().toJSON(), buildTgConfigCommand().toJSON(), buildTgAdminCommand().toJSON(), ], }); console.log("Slash commands registered."); } async function onPollOpen(slot: TGSlot): Promise { const channelId = Config.get({ section: "channels", key: "poll" }); const channel = await client.channels.fetch(channelId) as any; if (!channel) return console.error("Poll channel not found."); await postPoll(channel, slot); } // Fires at tgHour exactly (e.g. 20:00) — voting closes, lockedYesKeys snapshotted async function onPollLock(slot: TGSlot): Promise { const state = polls.get(slot.tgHour); if (!state || state.locked) return; lockPoll(slot.tgHour); const channelId = Config.get({ section: "channels", key: "poll" }); const channel = await client.channels.fetch(channelId) as any; if (!channel) return; // Buttons disabled, no submit button yet — that comes at close await updatePollMessage(channel, slot.tgHour); console.log(`[${new Date().toISOString()}] Poll locked for ${slot.tgHour}:00.`); } // Fires at tgHour + closesAfter (e.g. 20:35) — TG ended, reveal Submit Score. // Fires regardless of whether /tg call already opened it early — harmless // no-op in that case since scoreSubmitOpen is already true. async function onPollClose(slot: TGSlot): Promise { const state = polls.get(slot.tgHour); if (!state) return; const channelId = Config.get({ section: "channels", key: "poll" }); const channel = await client.channels.fetch(channelId) as any; if (!channel) return; state.scoreSubmitOpen = true; persist.save(polls); await updatePollMessage(channel, slot.tgHour, undefined, true); // showSubmit = true console.log(`[${new Date().toISOString()}] Poll closed for ${slot.tgHour}:00.`); } // Fires at tgHour - sleepCheckMinutesBefore (e.g. 19:40 for a 20:00 TG, // default 20 min) — sweeps current Yes voters for the sleepCheck role. async function onSleepCheck(slot: TGSlot): Promise { await SleepCheck.sweepPollForSleepCheck(client, slot); } client.on("interactionCreate", handleInteraction); client.once("clientReady", async () => { console.log(`Logged in as ${client.user!.tag}`); await Runtime.start(); DiscordClient.set(client); // Register event handlers RuntimeEvents.on("scoreSubmitted", async ({ historyKey }) => { await Leaderboard.update(); if (Attendance.allSubmitted(historyKey)) { RuntimeEvents.emit("allScoresSubmitted", { historyKey }); } }); RuntimeEvents.on("allScoresSubmitted", async ({ historyKey }) => { await Result.post({ historyKey }); }); const restored = persist.load(); if (restored) { for (const [slot, state] of restored) polls.set(slot, state); // Re-render all restored poll messages const channelId = Config.get({ section: "channels", key: "poll" }); const channel = await client.channels.fetch(channelId) as any; for (const slot of polls.keys()) { const state = polls.get(slot)!; await updatePollMessage(channel, slot, undefined, state.locked && state.confirmed === null); } console.log("Poll state restored and messages re-rendered."); } const guild = await client.guilds.fetch(GUILD_ID); await guild.members.fetch(); console.log(`Member cache warmed: ${guild.members.cache.size} members`); if (process.argv.includes("--register")) { await registerCommands(); } Scheduler.schedule(client, onPollOpen, onPollLock, onPollClose, onSleepCheck); console.log("Bot ready."); }); client.login(TOKEN);