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 } from "@systems/runtime"; 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 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; await updatePollMessage(channel, slot.tgHour, undefined, true); // showSubmit = true console.log(`[${new Date().toISOString()}] Poll closed for ${slot.tgHour}:00.`); } client.on("interactionCreate", handleInteraction); client.once("clientReady", async () => { console.log(`Logged in as ${client.user!.tag}`); await Runtime.start(); 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); console.log("Bot ready."); }); client.login(TOKEN);