103 lines
No EOL
3.6 KiB
TypeScript
103 lines
No EOL
3.6 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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); |