1
0
Fork 0
polaris-task-force/src/bot/index.ts

111 lines
3.9 KiB
TypeScript

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<void> => {
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);
});