From 9cfbf5ec578c40b7eb762b336a667ee38cf272b7 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 17 May 2026 23:14:31 -0700 Subject: [PATCH] Refine Discord draft order notification: league name/link, usernames, polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace seasonName with leagueName + leagueUrl; title links to the league - Distinguish manual vs randomized in title ("Manually Set" vs "Randomized") - Show owner username next to each team name in the draft order list - Remove footer in favour of the linked embed title - Add 4096-char description clamp consistent with standings notification - Use a teamMap in both intent branches to avoid O(n²) find-in-loop - Add test for all-unowned-teams rendering no trailing parentheses Co-Authored-By: Claude Sonnet 4.6 --- .../leagues/$leagueId.settings.server.ts | 46 ++++++++--- app/services/__tests__/discord.test.ts | 81 ++++++++++++++++--- app/services/discord.ts | 26 ++++-- 3 files changed, 120 insertions(+), 33 deletions(-) diff --git a/app/routes/leagues/$leagueId.settings.server.ts b/app/routes/leagues/$leagueId.settings.server.ts index b04b97b..18bc795 100644 --- a/app/routes/leagues/$leagueId.settings.server.ts +++ b/app/routes/leagues/$leagueId.settings.server.ts @@ -294,6 +294,8 @@ export async function action(args: Route.ActionArgs) { await setDraftOrder(season.id, teamIds); + const teamMap = new Map(teams.map((t) => [t.id, t])); + await logCommissionerAction({ seasonId: season.id, leagueId, @@ -303,7 +305,7 @@ export async function action(args: Route.ActionArgs) { details: { order: teamIds.map((id, i) => ({ teamId: id, - teamName: teams.find((t) => t.id === id)?.name ?? id, + teamName: teamMap.get(id)?.name ?? id, position: i + 1, })), }, @@ -311,14 +313,23 @@ export async function action(args: Route.ActionArgs) { if (league.discordWebhookUrl) { try { + const ownerIds = teams.map((t) => t.ownerId).filter(Boolean) as string[]; + const owners = ownerIds.length > 0 ? await findUsersByIds(ownerIds) : []; + const ownerMap = new Map(owners.map((u) => [u.id, getUserDisplayName(u) ?? undefined])); + const appUrl = process.env.APP_URL ?? "https://brackt.com"; await sendDraftOrderNotification({ webhookUrl: league.discordWebhookUrl, - seasonName: season.name, + leagueName: league.name, + leagueUrl: `${appUrl}/leagues/${leagueId}`, method: "manual", - teams: teamIds.map((id, i) => ({ - name: teams.find((t) => t.id === id)?.name ?? id, - position: i + 1, - })), + teams: teamIds.map((id, i) => { + const team = teamMap.get(id); + return { + name: team?.name ?? id, + position: i + 1, + username: team?.ownerId ? ownerMap.get(team.ownerId) : undefined, + }; + }), }); } catch (err) { logger.error("Discord draft order notification failed:", err); @@ -341,6 +352,8 @@ export async function action(args: Route.ActionArgs) { const newSlots = await findDraftSlotsBySeasonId(season.id); const sortedSlots = newSlots.toSorted((a, b) => a.draftOrder - b.draftOrder); + const teamMap = new Map(teams.map((t) => [t.id, t])); + await logCommissionerAction({ seasonId: season.id, leagueId, @@ -350,7 +363,7 @@ export async function action(args: Route.ActionArgs) { details: { order: sortedSlots.map((s) => ({ teamId: s.teamId, - teamName: teams.find((t) => t.id === s.teamId)?.name ?? s.teamId, + teamName: teamMap.get(s.teamId)?.name ?? s.teamId, position: s.draftOrder, })), }, @@ -358,14 +371,23 @@ export async function action(args: Route.ActionArgs) { if (league.discordWebhookUrl) { try { + const ownerIds = teams.map((t) => t.ownerId).filter(Boolean) as string[]; + const owners = ownerIds.length > 0 ? await findUsersByIds(ownerIds) : []; + const ownerMap = new Map(owners.map((u) => [u.id, getUserDisplayName(u) ?? undefined])); + const appUrl = process.env.APP_URL ?? "https://brackt.com"; await sendDraftOrderNotification({ webhookUrl: league.discordWebhookUrl, - seasonName: season.name, + leagueName: league.name, + leagueUrl: `${appUrl}/leagues/${leagueId}`, method: "randomized", - teams: sortedSlots.map((s) => ({ - name: teams.find((t) => t.id === s.teamId)?.name ?? s.teamId, - position: s.draftOrder, - })), + teams: sortedSlots.map((s) => { + const team = teamMap.get(s.teamId); + return { + name: team?.name ?? s.teamId, + position: s.draftOrder, + username: team?.ownerId ? ownerMap.get(team.ownerId) : undefined, + }; + }), }); } catch (err) { logger.error("Discord draft order notification failed:", err); diff --git a/app/services/__tests__/discord.test.ts b/app/services/__tests__/discord.test.ts index 19d11c3..04db22d 100644 --- a/app/services/__tests__/discord.test.ts +++ b/app/services/__tests__/discord.test.ts @@ -448,34 +448,50 @@ describe("sendDraftOrderNotification", () => { vi.stubGlobal("fetch", mockFetch(204)); }); - it("uses 'šŸ“‹ Draft Order Set' title for manual method", async () => { + it("uses 'šŸ“‹ Draft Order Manually Set' title for manual method", async () => { await sendDraftOrderNotification({ webhookUrl: WEBHOOK_URL, - seasonName: "My League 2025", + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", method: "manual", teams: [{ name: "Team Alpha", position: 1 }], }); const payload = getPayload(); - expect(payload.embeds[0].title).toBe("šŸ“‹ Draft Order Set — My League 2025"); + expect(payload.embeds[0].title).toBe("šŸ“‹ Draft Order Manually Set — My League"); }); it("uses 'šŸŽ² Draft Order Randomized' title for randomized method", async () => { await sendDraftOrderNotification({ webhookUrl: WEBHOOK_URL, - seasonName: "My League 2025", + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", method: "randomized", teams: [{ name: "Team Alpha", position: 1 }], }); const payload = getPayload(); - expect(payload.embeds[0].title).toBe("šŸŽ² Draft Order Randomized — My League 2025"); + expect(payload.embeds[0].title).toBe("šŸ“‹ Draft Order Randomized — My League"); + }); + + it("links the embed title to the league URL", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", + method: "manual", + teams: [{ name: "Team Alpha", position: 1 }], + }); + + const payload = getPayload(); + expect(payload.embeds[0].url).toBe("https://brackt.com/leagues/abc"); }); it("renders a numbered list of teams sorted by position", async () => { await sendDraftOrderNotification({ webhookUrl: WEBHOOK_URL, - seasonName: "My League 2025", + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", method: "manual", teams: [ { name: "Team Gamma", position: 3 }, @@ -490,39 +506,61 @@ describe("sendDraftOrderNotification", () => { ); }); - it("escapes Discord markdown in team names", async () => { + it("shows username in parentheses next to team name when provided", async () => { await sendDraftOrderNotification({ webhookUrl: WEBHOOK_URL, - seasonName: "My League 2025", + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", method: "manual", teams: [ - { name: "Team__Underline", position: 1 }, + { name: "Team Alpha", position: 1, username: "chris" }, + { name: "Team Beta", position: 2 }, + ], + }); + + const payload = getPayload(); + expect(payload.embeds[0].description).toContain("1. Team Alpha (chris)"); + expect(payload.embeds[0].description).toContain("2. Team Beta"); + expect(payload.embeds[0].description).not.toContain("2. Team Beta ("); + }); + + it("escapes Discord markdown in team names and usernames", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", + method: "manual", + teams: [ + { name: "Team__Underline", position: 1, username: "user__name" }, { name: "Team*Bold*", position: 2 }, ], }); const payload = getPayload(); expect(payload.embeds[0].description).toContain("Team\\_\\_Underline"); + expect(payload.embeds[0].description).toContain("user\\_\\_name"); expect(payload.embeds[0].description).toContain("Team\\*Bold\\*"); }); - it("uses color 0x5865f2 and footer 'brackt.com'", async () => { + it("uses color 0x5865f2 and has no footer", async () => { await sendDraftOrderNotification({ webhookUrl: WEBHOOK_URL, - seasonName: "My League 2025", + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", 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" }); + expect(payload.embeds[0].footer).toBeUndefined(); }); it("does not set content or allowed_mentions", async () => { await sendDraftOrderNotification({ webhookUrl: WEBHOOK_URL, - seasonName: "My League 2025", + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", method: "manual", teams: [{ name: "Team Alpha", position: 1 }], }); @@ -531,4 +569,21 @@ describe("sendDraftOrderNotification", () => { expect(payload.content).toBeUndefined(); expect(payload.allowed_mentions).toBeUndefined(); }); + + it("renders bare team names with no trailing parentheses when no team has an owner", async () => { + await sendDraftOrderNotification({ + webhookUrl: WEBHOOK_URL, + leagueName: "My League", + leagueUrl: "https://brackt.com/leagues/abc", + method: "manual", + teams: [ + { 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"); + expect(payload.embeds[0].description).not.toContain("("); + }); }); diff --git a/app/services/discord.ts b/app/services/discord.ts index a0a17e1..b26b638 100644 --- a/app/services/discord.ts +++ b/app/services/discord.ts @@ -2,6 +2,7 @@ import { buildTiedRankChecker } from "~/lib/standings-display"; interface DiscordEmbed { title?: string; + url?: string; description?: string; color?: number; footer?: { text: string }; @@ -191,32 +192,41 @@ export async function sendStandingsUpdateNotification({ export async function sendDraftOrderNotification({ webhookUrl, - seasonName, + leagueName, + leagueUrl, method, teams, }: { webhookUrl: string; - seasonName: string; + leagueName: string; + leagueUrl: string; method: "manual" | "randomized"; - teams: Array<{ name: string; position: number }>; + teams: Array<{ name: string; position: number; username?: string }>; }): Promise { const title = method === "manual" - ? `šŸ“‹ Draft Order Set — ${seasonName}` - : `šŸŽ² Draft Order Randomized — ${seasonName}`; + ? `šŸ“‹ Draft Order Manually Set — ${leagueName}` + : `šŸ“‹ Draft Order Randomized — ${leagueName}`; const sorted = [...teams].sort((a, b) => a.position - b.position); - const description = sorted - .map((t) => `${t.position}. ${escapeMarkdown(t.name)}`) + let description = sorted + .map((t) => { + const teamLabel = escapeMarkdown(t.name); + const usernameLabel = t.username ? ` (${escapeMarkdown(t.username)})` : ""; + return `${t.position}. ${teamLabel}${usernameLabel}`; + }) .join("\n"); + if (description.length > 4096) { + description = description.slice(0, 4093) + "..."; + } await sendDiscordWebhook(webhookUrl, { embeds: [ { title, + url: leagueUrl, description, color: 0x5865f2, - footer: { text: "brackt.com" }, }, ], });