Add Discord notification when draft order is set or randomized
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
This commit is contained in:
parent
1f0fcf970b
commit
f9a3b7d90a
3 changed files with 157 additions and 2 deletions
|
|
@ -23,7 +23,7 @@ import { claimTeam, deleteTeam, findTeamById, findTeamsBySeasonId, removeTeamOwn
|
||||||
import { findUserById, findUsersByIds, getUserDisplayName, isUserAdmin } from "~/models/user";
|
import { findUserById, findUsersByIds, getUserDisplayName, isUserAdmin } from "~/models/user";
|
||||||
import { logCommissionerAction } from "~/models/audit-log";
|
import { logCommissionerAction } from "~/models/audit-log";
|
||||||
import { parseDraftSpeed } from "~/lib/draft-timer";
|
import { parseDraftSpeed } from "~/lib/draft-timer";
|
||||||
import { sendStandingsUpdateNotification } from "~/services/discord";
|
import { sendDraftOrderNotification, sendStandingsUpdateNotification } from "~/services/discord";
|
||||||
import {
|
import {
|
||||||
applyBracktSportsForSeason,
|
applyBracktSportsForSeason,
|
||||||
seedOrRepairBracktTemplate,
|
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 };
|
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 };
|
return { success: true, message: "Draft order randomized successfully", section: "draft-order" as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
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";
|
const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc";
|
||||||
|
|
||||||
|
|
@ -442,3 +442,93 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
expect(desc).not.toContain("Beta");
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -188,3 +188,36 @@ export async function sendStandingsUpdateNotification({
|
||||||
|
|
||||||
await sendDiscordWebhook(webhookUrl, payload);
|
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<void> {
|
||||||
|
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" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue