From f9a3b7d90a67d4ce48f1816099f77d770c807d05 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 05:56:09 +0000 Subject: [PATCH] Add Discord notification when draft order is set or randomized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts the full draft order to the league's Discord webhook after each set-draft-order or randomize-draft-order action, with distinct titles (📋 vs 🎲) so members can tell how it was determined. https://claude.ai/code/session_01XrBxu358RK6q8iDgRQbMFx --- .../leagues/$leagueId.settings.server.ts | 34 ++++++- app/services/__tests__/discord.test.ts | 92 ++++++++++++++++++- app/services/discord.ts | 33 +++++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/app/routes/leagues/$leagueId.settings.server.ts b/app/routes/leagues/$leagueId.settings.server.ts index 768978f..b04b97b 100644 --- a/app/routes/leagues/$leagueId.settings.server.ts +++ b/app/routes/leagues/$leagueId.settings.server.ts @@ -23,7 +23,7 @@ import { claimTeam, deleteTeam, findTeamById, findTeamsBySeasonId, removeTeamOwn import { findUserById, findUsersByIds, getUserDisplayName, isUserAdmin } from "~/models/user"; import { logCommissionerAction } from "~/models/audit-log"; import { parseDraftSpeed } from "~/lib/draft-timer"; -import { sendStandingsUpdateNotification } from "~/services/discord"; +import { sendDraftOrderNotification, sendStandingsUpdateNotification } from "~/services/discord"; import { applyBracktSportsForSeason, seedOrRepairBracktTemplate, @@ -309,6 +309,22 @@ export async function action(args: Route.ActionArgs) { }, }); + if (league.discordWebhookUrl) { + try { + await sendDraftOrderNotification({ + webhookUrl: league.discordWebhookUrl, + seasonName: season.name, + method: "manual", + teams: teamIds.map((id, i) => ({ + name: teams.find((t) => t.id === id)?.name ?? id, + position: i + 1, + })), + }); + } catch (err) { + logger.error("Discord draft order notification failed:", err); + } + } + return { success: true, message: "Draft order updated successfully", section: "draft-order" as const }; } @@ -340,6 +356,22 @@ export async function action(args: Route.ActionArgs) { }, }); + if (league.discordWebhookUrl) { + try { + await sendDraftOrderNotification({ + webhookUrl: league.discordWebhookUrl, + seasonName: season.name, + method: "randomized", + teams: sortedSlots.map((s) => ({ + name: teams.find((t) => t.id === s.teamId)?.name ?? s.teamId, + position: s.draftOrder, + })), + }); + } catch (err) { + logger.error("Discord draft order notification failed:", err); + } + } + return { success: true, message: "Draft order randomized successfully", section: "draft-order" as const }; } diff --git a/app/services/__tests__/discord.test.ts b/app/services/__tests__/discord.test.ts index 4ac94eb..19d11c3 100644 --- a/app/services/__tests__/discord.test.ts +++ b/app/services/__tests__/discord.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { sendDiscordWebhook, sendStandingsUpdateNotification } from "../discord"; +import { sendDiscordWebhook, sendStandingsUpdateNotification, sendDraftOrderNotification } from "../discord"; const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc"; @@ -442,3 +442,93 @@ describe("sendStandingsUpdateNotification", () => { expect(desc).not.toContain("Beta"); }); }); + +describe("sendDraftOrderNotification", () => { + beforeEach(() => { + vi.stubGlobal("fetch", mockFetch(204)); + }); + + it("uses '📋 Draft Order Set' title for manual method", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + method: "manual", + teams: [{ name: "Team Alpha", position: 1 }], + }); + + const payload = getPayload(); + expect(payload.embeds[0].title).toBe("📋 Draft Order Set — My League 2025"); + }); + + it("uses '🎲 Draft Order Randomized' title for randomized method", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + method: "randomized", + teams: [{ name: "Team Alpha", position: 1 }], + }); + + const payload = getPayload(); + expect(payload.embeds[0].title).toBe("🎲 Draft Order Randomized — My League 2025"); + }); + + it("renders a numbered list of teams sorted by position", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + method: "manual", + teams: [ + { name: "Team Gamma", position: 3 }, + { name: "Team Alpha", position: 1 }, + { name: "Team Beta", position: 2 }, + ], + }); + + const payload = getPayload(); + expect(payload.embeds[0].description).toBe( + "1. Team Alpha\n2. Team Beta\n3. Team Gamma" + ); + }); + + it("escapes Discord markdown in team names", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + method: "manual", + teams: [ + { name: "Team__Underline", position: 1 }, + { name: "Team*Bold*", position: 2 }, + ], + }); + + const payload = getPayload(); + expect(payload.embeds[0].description).toContain("Team\\_\\_Underline"); + expect(payload.embeds[0].description).toContain("Team\\*Bold\\*"); + }); + + it("uses color 0x5865f2 and footer 'brackt.com'", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + method: "manual", + teams: [{ name: "Team Alpha", position: 1 }], + }); + + const payload = getPayload(); + expect(payload.embeds[0].color).toBe(0x5865f2); + expect(payload.embeds[0].footer).toEqual({ text: "brackt.com" }); + }); + + it("does not set content or allowed_mentions", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + method: "manual", + teams: [{ name: "Team Alpha", position: 1 }], + }); + + const payload = getPayload(); + expect(payload.content).toBeUndefined(); + expect(payload.allowed_mentions).toBeUndefined(); + }); +}); diff --git a/app/services/discord.ts b/app/services/discord.ts index 70126c5..a0a17e1 100644 --- a/app/services/discord.ts +++ b/app/services/discord.ts @@ -188,3 +188,36 @@ export async function sendStandingsUpdateNotification({ await sendDiscordWebhook(webhookUrl, payload); } + +export async function sendDraftOrderNotification({ + webhookUrl, + seasonName, + method, + teams, +}: { + webhookUrl: string; + seasonName: string; + method: "manual" | "randomized"; + teams: Array<{ name: string; position: number }>; +}): Promise { + const title = + method === "manual" + ? `📋 Draft Order Set — ${seasonName}` + : `🎲 Draft Order Randomized — ${seasonName}`; + + const sorted = [...teams].sort((a, b) => a.position - b.position); + const description = sorted + .map((t) => `${t.position}. ${escapeMarkdown(t.name)}`) + .join("\n"); + + await sendDiscordWebhook(webhookUrl, { + embeds: [ + { + title, + description, + color: 0x5865f2, + footer: { text: "brackt.com" }, + }, + ], + }); +}