Refine Discord draft order notification: league name/link, usernames, polish
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
f9a3b7d90a
commit
9cfbf5ec57
3 changed files with 120 additions and 33 deletions
|
|
@ -294,6 +294,8 @@ export async function action(args: Route.ActionArgs) {
|
||||||
|
|
||||||
await setDraftOrder(season.id, teamIds);
|
await setDraftOrder(season.id, teamIds);
|
||||||
|
|
||||||
|
const teamMap = new Map(teams.map((t) => [t.id, t]));
|
||||||
|
|
||||||
await logCommissionerAction({
|
await logCommissionerAction({
|
||||||
seasonId: season.id,
|
seasonId: season.id,
|
||||||
leagueId,
|
leagueId,
|
||||||
|
|
@ -303,7 +305,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
details: {
|
details: {
|
||||||
order: teamIds.map((id, i) => ({
|
order: teamIds.map((id, i) => ({
|
||||||
teamId: id,
|
teamId: id,
|
||||||
teamName: teams.find((t) => t.id === id)?.name ?? id,
|
teamName: teamMap.get(id)?.name ?? id,
|
||||||
position: i + 1,
|
position: i + 1,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|
@ -311,14 +313,23 @@ export async function action(args: Route.ActionArgs) {
|
||||||
|
|
||||||
if (league.discordWebhookUrl) {
|
if (league.discordWebhookUrl) {
|
||||||
try {
|
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({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: league.discordWebhookUrl,
|
webhookUrl: league.discordWebhookUrl,
|
||||||
seasonName: season.name,
|
leagueName: league.name,
|
||||||
|
leagueUrl: `${appUrl}/leagues/${leagueId}`,
|
||||||
method: "manual",
|
method: "manual",
|
||||||
teams: teamIds.map((id, i) => ({
|
teams: teamIds.map((id, i) => {
|
||||||
name: teams.find((t) => t.id === id)?.name ?? id,
|
const team = teamMap.get(id);
|
||||||
position: i + 1,
|
return {
|
||||||
})),
|
name: team?.name ?? id,
|
||||||
|
position: i + 1,
|
||||||
|
username: team?.ownerId ? ownerMap.get(team.ownerId) : undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Discord draft order notification failed:", 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 newSlots = await findDraftSlotsBySeasonId(season.id);
|
||||||
const sortedSlots = newSlots.toSorted((a, b) => a.draftOrder - b.draftOrder);
|
const sortedSlots = newSlots.toSorted((a, b) => a.draftOrder - b.draftOrder);
|
||||||
|
|
||||||
|
const teamMap = new Map(teams.map((t) => [t.id, t]));
|
||||||
|
|
||||||
await logCommissionerAction({
|
await logCommissionerAction({
|
||||||
seasonId: season.id,
|
seasonId: season.id,
|
||||||
leagueId,
|
leagueId,
|
||||||
|
|
@ -350,7 +363,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
details: {
|
details: {
|
||||||
order: sortedSlots.map((s) => ({
|
order: sortedSlots.map((s) => ({
|
||||||
teamId: s.teamId,
|
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,
|
position: s.draftOrder,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|
@ -358,14 +371,23 @@ export async function action(args: Route.ActionArgs) {
|
||||||
|
|
||||||
if (league.discordWebhookUrl) {
|
if (league.discordWebhookUrl) {
|
||||||
try {
|
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({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: league.discordWebhookUrl,
|
webhookUrl: league.discordWebhookUrl,
|
||||||
seasonName: season.name,
|
leagueName: league.name,
|
||||||
|
leagueUrl: `${appUrl}/leagues/${leagueId}`,
|
||||||
method: "randomized",
|
method: "randomized",
|
||||||
teams: sortedSlots.map((s) => ({
|
teams: sortedSlots.map((s) => {
|
||||||
name: teams.find((t) => t.id === s.teamId)?.name ?? s.teamId,
|
const team = teamMap.get(s.teamId);
|
||||||
position: s.draftOrder,
|
return {
|
||||||
})),
|
name: team?.name ?? s.teamId,
|
||||||
|
position: s.draftOrder,
|
||||||
|
username: team?.ownerId ? ownerMap.get(team.ownerId) : undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Discord draft order notification failed:", err);
|
logger.error("Discord draft order notification failed:", err);
|
||||||
|
|
|
||||||
|
|
@ -448,34 +448,50 @@ describe("sendDraftOrderNotification", () => {
|
||||||
vi.stubGlobal("fetch", mockFetch(204));
|
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({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
leagueName: "My League",
|
||||||
|
leagueUrl: "https://brackt.com/leagues/abc",
|
||||||
method: "manual",
|
method: "manual",
|
||||||
teams: [{ name: "Team Alpha", position: 1 }],
|
teams: [{ name: "Team Alpha", position: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = getPayload();
|
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 () => {
|
it("uses '🎲 Draft Order Randomized' title for randomized method", async () => {
|
||||||
await sendDraftOrderNotification({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
leagueName: "My League",
|
||||||
|
leagueUrl: "https://brackt.com/leagues/abc",
|
||||||
method: "randomized",
|
method: "randomized",
|
||||||
teams: [{ name: "Team Alpha", position: 1 }],
|
teams: [{ name: "Team Alpha", position: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = getPayload();
|
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 () => {
|
it("renders a numbered list of teams sorted by position", async () => {
|
||||||
await sendDraftOrderNotification({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
leagueName: "My League",
|
||||||
|
leagueUrl: "https://brackt.com/leagues/abc",
|
||||||
method: "manual",
|
method: "manual",
|
||||||
teams: [
|
teams: [
|
||||||
{ name: "Team Gamma", position: 3 },
|
{ 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({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
leagueName: "My League",
|
||||||
|
leagueUrl: "https://brackt.com/leagues/abc",
|
||||||
method: "manual",
|
method: "manual",
|
||||||
teams: [
|
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 },
|
{ name: "Team*Bold*", position: 2 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = getPayload();
|
const payload = getPayload();
|
||||||
expect(payload.embeds[0].description).toContain("Team\\_\\_Underline");
|
expect(payload.embeds[0].description).toContain("Team\\_\\_Underline");
|
||||||
|
expect(payload.embeds[0].description).toContain("user\\_\\_name");
|
||||||
expect(payload.embeds[0].description).toContain("Team\\*Bold\\*");
|
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({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
leagueName: "My League",
|
||||||
|
leagueUrl: "https://brackt.com/leagues/abc",
|
||||||
method: "manual",
|
method: "manual",
|
||||||
teams: [{ name: "Team Alpha", position: 1 }],
|
teams: [{ name: "Team Alpha", position: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = getPayload();
|
const payload = getPayload();
|
||||||
expect(payload.embeds[0].color).toBe(0x5865f2);
|
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 () => {
|
it("does not set content or allowed_mentions", async () => {
|
||||||
await sendDraftOrderNotification({
|
await sendDraftOrderNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
leagueName: "My League",
|
||||||
|
leagueUrl: "https://brackt.com/leagues/abc",
|
||||||
method: "manual",
|
method: "manual",
|
||||||
teams: [{ name: "Team Alpha", position: 1 }],
|
teams: [{ name: "Team Alpha", position: 1 }],
|
||||||
});
|
});
|
||||||
|
|
@ -531,4 +569,21 @@ describe("sendDraftOrderNotification", () => {
|
||||||
expect(payload.content).toBeUndefined();
|
expect(payload.content).toBeUndefined();
|
||||||
expect(payload.allowed_mentions).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("(");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { buildTiedRankChecker } from "~/lib/standings-display";
|
||||||
|
|
||||||
interface DiscordEmbed {
|
interface DiscordEmbed {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
url?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
color?: number;
|
color?: number;
|
||||||
footer?: { text: string };
|
footer?: { text: string };
|
||||||
|
|
@ -191,32 +192,41 @@ export async function sendStandingsUpdateNotification({
|
||||||
|
|
||||||
export async function sendDraftOrderNotification({
|
export async function sendDraftOrderNotification({
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
seasonName,
|
leagueName,
|
||||||
|
leagueUrl,
|
||||||
method,
|
method,
|
||||||
teams,
|
teams,
|
||||||
}: {
|
}: {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
seasonName: string;
|
leagueName: string;
|
||||||
|
leagueUrl: string;
|
||||||
method: "manual" | "randomized";
|
method: "manual" | "randomized";
|
||||||
teams: Array<{ name: string; position: number }>;
|
teams: Array<{ name: string; position: number; username?: string }>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const title =
|
const title =
|
||||||
method === "manual"
|
method === "manual"
|
||||||
? `📋 Draft Order Set — ${seasonName}`
|
? `📋 Draft Order Manually Set — ${leagueName}`
|
||||||
: `🎲 Draft Order Randomized — ${seasonName}`;
|
: `📋 Draft Order Randomized — ${leagueName}`;
|
||||||
|
|
||||||
const sorted = [...teams].sort((a, b) => a.position - b.position);
|
const sorted = [...teams].sort((a, b) => a.position - b.position);
|
||||||
const description = sorted
|
let description = sorted
|
||||||
.map((t) => `${t.position}. ${escapeMarkdown(t.name)}`)
|
.map((t) => {
|
||||||
|
const teamLabel = escapeMarkdown(t.name);
|
||||||
|
const usernameLabel = t.username ? ` (${escapeMarkdown(t.username)})` : "";
|
||||||
|
return `${t.position}. ${teamLabel}${usernameLabel}`;
|
||||||
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
if (description.length > 4096) {
|
||||||
|
description = description.slice(0, 4093) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
await sendDiscordWebhook(webhookUrl, {
|
await sendDiscordWebhook(webhookUrl, {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
|
url: leagueUrl,
|
||||||
description,
|
description,
|
||||||
color: 0x5865f2,
|
color: 0x5865f2,
|
||||||
footer: { text: "brackt.com" },
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue