/** * @file Discord webhook and role-grant utilities for milestone events. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable @typescript-eslint/naming-convention -- Discord API requires snake_case and Pascal-case header names */ import { logger } from "./logger.js"; const discordApi = "https://discord.com/api/v10"; /** * Discord MessageFlags.SUPPRESS_NOTIFICATIONS — messages are delivered without * triggering desktop or mobile push notifications. */ const suppressNotifications = 4096; /** * Grants the apotheosis Discord role to the given player if configured. * Fails silently so role grant errors do not affect the game action. * @param discordId - The Discord user ID to grant the role to. * @returns A promise that resolves when the role grant attempt completes. */ const grantApotheosisRole = async(discordId: string): Promise => { const botToken = process.env.DISCORD_BOT_TOKEN; const guildId = process.env.DISCORD_GUILD_ID; const roleId = process.env.DISCORD_APOTHEOSIS_ROLE_ID; if ( botToken === undefined || botToken === "" || guildId === undefined || guildId === "" || roleId === undefined || roleId === "" ) { return; } try { await fetch( `${discordApi}/guilds/${guildId}/members/${discordId}/roles/${roleId}`, { headers: { Authorization: `Bot ${botToken}` }, method: "PUT", }, ); } catch (error) { void logger.error( "webhook_apotheosis_role", error instanceof Error ? error : new Error(String(error)), ); // Graceful degradation — role grant failure must not affect the apotheosis } }; type MilestoneType = "prestige" | "transcendence" | "apotheosis"; interface MilestoneCounts { prestige: number; transcendence: number; apotheosis: number; } const milestoneVerbs: Record = { apotheosis: "reached apotheosis", prestige: "prestiged", transcendence: "transcended", }; /** * Posts a milestone announcement to the configured Discord webhook. * Fails silently so webhook errors do not affect the game action. * @param discordId - The Discord user ID of the player. * @param milestone - The type of milestone reached. * @param counts - The current prestige, transcendence, and apotheosis counts. * @returns A promise that resolves when the webhook post attempt completes. */ const postMilestoneWebhook = async( discordId: string, milestone: MilestoneType, counts: MilestoneCounts, ): Promise => { const webhookUrl = process.env.DISCORD_MILESTONE_WEBHOOK; if (webhookUrl === undefined || webhookUrl === "") { return; } const verb = milestoneVerbs[milestone]; /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- counts fields are numbers, intentionally stringified */ const content = `<@${discordId}> has ${verb}~! They are now on Prestige ${counts.prestige}, Transcendence ${counts.transcendence}, Apotheosis ${counts.apotheosis}!`; try { await fetch(webhookUrl, { body: JSON.stringify({ content: content, flags: suppressNotifications, }), headers: { "Content-Type": "application/json" }, method: "POST", }); } catch (error) { void logger.error( "webhook_milestone", error instanceof Error ? error : new Error(String(error)), ); // Graceful degradation — webhook failure must not affect the game action } }; export { grantApotheosisRole, postMilestoneWebhook };