import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js"; import { getPayload } from "payload"; import config from "@payload-config"; import { botConfig } from "./config"; import { commands } from "./commands"; import { services } from "./services"; import { handleButtonInteraction } from "./events/interactionCreate"; import { handleTransferInteraction } from "./events/transferInteractions"; // Usage: `bun run bot`. Standalone long-running process; connects to Discord // and shares the Postgres database with the web app via the Payload config. const main = async () => { const payload = await getPayload({ config }); const client = new Client({ intents: [GatewayIntentBits.Guilds], }); let shuttingDown = false; const shutdown = async (signal: NodeJS.Signals): Promise => { if (shuttingDown) { payload.logger.warn(`[Bot] ${signal} received again, forcing exit`); process.exit(143); } shuttingDown = true; payload.logger.info(`[Bot] ${signal} received, shutting down gracefully`); try { if (client.isReady()) { await client.destroy(); } payload.logger.info("[Bot] Discord client destroyed"); process.exit(0); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; payload.logger.error(`[Bot] Error during shutdown: ${message}`); process.exit(1); } }; process.on("SIGTERM", () => void shutdown("SIGTERM")); process.on("SIGINT", () => void shutdown("SIGINT")); const commandExecutors = new Map( commands.map((command) => [command.builder.name, command.execute]), ); client.once(Events.ClientReady, async (readyClient) => { payload.logger.info(`[Bot] Logged in as ${readyClient.user.tag}`); const globalCommands = commands.filter((command) => command.scope === "global"); const guildCommands = commands.filter((command) => command.scope !== "global"); // Guild-scoped commands are never exposed in DMs — /signup and /link must // be global so they actually appear (and run) in a DM with the bot. if (globalCommands.length > 0) { await readyClient.application.commands.set( globalCommands.map((command) => command.builder.toJSON()), ); } if (guildCommands.length > 0) { await readyClient.application.commands.set( guildCommands.map((command) => command.builder.toJSON()), botConfig.guildId, ); } payload.logger.info( `[Bot] Registered ${globalCommands.length} global and ${guildCommands.length} guild command(s)`, ); for (const start of services) { await start(readyClient, payload); } }); client.on(Events.InteractionCreate, async (interaction) => { if (interaction.isButton() && interaction.customId.startsWith("ptf-att:")) { await handleButtonInteraction(interaction, payload); return; } if ( (interaction.isButton() || interaction.isModalSubmit()) && interaction.customId.startsWith("ptf-xfer:") ) { await handleTransferInteraction(interaction, payload); return; } if (!interaction.isChatInputCommand()) { return; } const execute = commandExecutors.get(interaction.commandName); if (!execute) { return; } try { await execute(interaction, payload); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; payload.logger.error(`[Bot] Error executing /${interaction.commandName}: ${message}`); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: "Something went wrong while running this command.", flags: MessageFlags.Ephemeral, }); } } }); await client.login(botConfig.token); }; main().catch((error) => { const message = error instanceof Error ? error.message : "Unknown error"; console.error(`[Bot] Failed to start: ${message}`); process.exit(1); });