diff --git a/apps/api/prod.env b/apps/api/prod.env index 5789456..049594d 100644 --- a/apps/api/prod.env +++ b/apps/api/prod.env @@ -5,4 +5,5 @@ JWT_SECRET="op://Environment Variables - Naomi/Elysium/jwt secret" DATABASE_URL="op://Environment Variables - Naomi/Elysium/mongo url" ANTI_CHEAT_SECRET="op://Environment Variables - Naomi/Elysium/anti cheat secret" PORT="op://Environment Variables - Naomi/Elysium/port" -CORS_ORIGIN="op://Environment Variables - Naomi/Elysium/origin" \ No newline at end of file +CORS_ORIGIN="op://Environment Variables - Naomi/Elysium/origin" +DISCORD_MILESTONE_WEBHOOK="op://Environment Variables - Naomi/Elysium/discord milestone webhook" \ No newline at end of file diff --git a/apps/api/src/routes/apotheosis.ts b/apps/api/src/routes/apotheosis.ts index c666fe9..cf35886 100644 --- a/apps/api/src/routes/apotheosis.ts +++ b/apps/api/src/routes/apotheosis.ts @@ -7,6 +7,7 @@ import { buildPostApotheosisState, isEligibleForApotheosis, } from "../services/apotheosis.js"; +import { postMilestoneWebhook } from "../services/webhook.js"; export const apotheosisRouter = new Hono(); @@ -61,5 +62,11 @@ apotheosisRouter.post("/", async (context) => { }, }); + void postMilestoneWebhook(discordId, "apotheosis", { + prestige: newState.prestige.count, + transcendence: newState.transcendence?.count ?? 0, + apotheosis: newApotheosisData.count, + }); + return context.json({ newApotheosisCount: newApotheosisData.count }); }); diff --git a/apps/api/src/routes/prestige.ts b/apps/api/src/routes/prestige.ts index 02c83e7..fded59d 100644 --- a/apps/api/src/routes/prestige.ts +++ b/apps/api/src/routes/prestige.ts @@ -10,6 +10,7 @@ import { computeRunestoneMultipliers, isEligibleForPrestige, } from "../services/prestige.js"; +import { postMilestoneWebhook } from "../services/webhook.js"; export const prestigeRouter = new Hono(); @@ -87,6 +88,12 @@ prestigeRouter.post("/", async (context) => { }, }); + void postMilestoneWebhook(discordId, "prestige", { + prestige: newPrestigeData.count, + transcendence: newState.transcendence?.count ?? 0, + apotheosis: newState.apotheosis?.count ?? 0, + }); + return context.json({ runestones: runestonesEarned, newPrestigeCount: newPrestigeData.count, diff --git a/apps/api/src/routes/transcendence.ts b/apps/api/src/routes/transcendence.ts index 0b00229..09861b4 100644 --- a/apps/api/src/routes/transcendence.ts +++ b/apps/api/src/routes/transcendence.ts @@ -9,6 +9,7 @@ import { computeTranscendenceMultipliers, isEligibleForTranscendence, } from "../services/transcendence.js"; +import { postMilestoneWebhook } from "../services/webhook.js"; export const transcendenceRouter = new Hono(); @@ -66,6 +67,12 @@ transcendenceRouter.post("/", async (context) => { }, }); + void postMilestoneWebhook(discordId, "transcendence", { + prestige: newState.prestige.count, + transcendence: newTranscendenceData.count, + apotheosis: newState.apotheosis?.count ?? 0, + }); + return context.json({ echoes: echoesEarned, newTranscendenceCount: newTranscendenceData.count, diff --git a/apps/api/src/services/webhook.ts b/apps/api/src/services/webhook.ts new file mode 100644 index 0000000..ac57aa0 --- /dev/null +++ b/apps/api/src/services/webhook.ts @@ -0,0 +1,37 @@ +type MilestoneType = "prestige" | "transcendence" | "apotheosis"; + +interface MilestoneCounts { + prestige: number; + transcendence: number; + apotheosis: number; +} + +const MILESTONE_VERBS: Record = { + prestige: "prestiged", + transcendence: "transcended", + apotheosis: "reached apotheosis", +}; + +export const postMilestoneWebhook = async ( + discordId: string, + milestone: MilestoneType, + counts: MilestoneCounts, +): Promise => { + const webhookUrl = process.env["DISCORD_MILESTONE_WEBHOOK"]; + if (!webhookUrl) { + return; + } + + const verb = MILESTONE_VERBS[milestone]; + const content = `<@${discordId}> has ${verb}~! They are now on Prestige ${counts.prestige}, Transcendence ${counts.transcendence}, Apotheosis ${counts.apotheosis}!`; + + try { + await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); + } catch { + // Graceful degradation — webhook failure must not affect the game action + } +};