stop spamming discord webhook
All checks were successful
check / check (push) Successful in 34s

This commit is contained in:
matamune 2026-05-13 22:47:57 +00:00
parent 34802ac712
commit fc87bc74f8
Signed by: matamune
GPG key ID: 3BB8E7D3B968A324
7 changed files with 40 additions and 12 deletions

View file

@ -18,6 +18,7 @@ PORT=3000
DATA_DIR=/app/data
JOJO_WEBHOOK_SECRET=...
GITHUB_WEBHOOK_SECRET=...
DISCORD_OUTPUT_ENABLED=false
DISCORD_WEBHOOK_URL=
DISCORD_NOTIFY_EVENTS=push,pull_request,release
FEED_SOURCES_PATH=./feed-sources.json
@ -25,9 +26,9 @@ PATCHBAY_FLOW_DISPATCH_URL=
PATCHBAY_FLOW_DISPATCH_SECRET=
```
Discord notifications are optional. When `DISCORD_WEBHOOK_URL` is unset, the
service skips Discord output. `DISCORD_NOTIFY_EVENTS` is a comma-separated
allow list and defaults to `push,pull_request,release`.
Discord notifications are off by default. Set `DISCORD_OUTPUT_ENABLED=true`
and `DISCORD_WEBHOOK_URL` to send Discord output. `DISCORD_NOTIFY_EVENTS` is a
comma-separated allow list and defaults to `push,pull_request,release`.
## Development

View file

@ -26,6 +26,7 @@ type DiscordPayload = {
type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
export type DiscordConfig = {
enabled: boolean;
webhookUrl?: string;
notifyEvents: Set<string>;
};
@ -38,7 +39,13 @@ export type DiscordNotification = {
const defaultNotifyEvents = ["push", "pull_request", "release"];
function parseEnabled(value?: string): boolean {
const normalized = value?.trim().toLowerCase();
return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
}
export function parseDiscordConfig(input: {
enabled?: string;
webhookUrl?: string;
notifyEvents?: string;
}): DiscordConfig {
@ -50,6 +57,7 @@ export function parseDiscordConfig(input: {
);
return {
enabled: parseEnabled(input.enabled),
webhookUrl: input.webhookUrl?.trim() || undefined,
notifyEvents,
};
@ -188,7 +196,7 @@ export async function notifyDiscord(
fetchImpl: FetchLike = fetch,
): Promise<void> {
const eventName = notification.signal?.event ?? notification.event?.event;
if (!config.webhookUrl || !eventName || !config.notifyEvents.has(eventName)) {
if (!config.enabled || !config.webhookUrl || !eventName || !config.notifyEvents.has(eventName)) {
return;
}

View file

@ -201,7 +201,7 @@ export async function pollFeedSource(input: {
flowDispatches += 1;
}
}
await notifyDiscord(input.discord ?? { notifyEvents: new Set() }, { signal, job });
await notifyDiscord(input.discord ?? { enabled: false, notifyEvents: new Set() }, { signal, job });
signals.push(signal);
console.log(JSON.stringify({
type: "feed.accepted",

View file

@ -208,6 +208,7 @@ if (import.meta.main) {
dataDir: process.env.DATA_DIR ?? "./data",
adminToken: process.env.PATCHBAY_ADMIN_TOKEN,
discord: parseDiscordConfig({
enabled: process.env.DISCORD_OUTPUT_ENABLED,
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
notifyEvents: process.env.DISCORD_NOTIFY_EVENTS,
}),

View file

@ -55,12 +55,20 @@ const feedSignal: FeedSignal = {
describe("discord notifications", () => {
test("parses default notify events", () => {
const config = parseDiscordConfig({});
expect(config.enabled).toBe(false);
expect(config.notifyEvents.has("push")).toBe(true);
expect(config.notifyEvents.has("pull_request")).toBe(true);
expect(config.notifyEvents.has("release")).toBe(true);
expect(config.notifyEvents.has("ping")).toBe(false);
});
test("parses explicit enable flag", () => {
expect(parseDiscordConfig({ enabled: "true" }).enabled).toBe(true);
expect(parseDiscordConfig({ enabled: "1" }).enabled).toBe(true);
expect(parseDiscordConfig({ enabled: "yes" }).enabled).toBe(true);
expect(parseDiscordConfig({ enabled: "false" }).enabled).toBe(false);
});
test("builds readable push embeds", () => {
const payload = buildDiscordPayload({
event: pushEvent,
@ -92,7 +100,16 @@ describe("discord notifications", () => {
test("does nothing without a webhook URL", async () => {
let calls = 0;
await notifyDiscord(parseDiscordConfig({}), { event: pushEvent }, async () => {
await notifyDiscord(parseDiscordConfig({ enabled: "true" }), { event: pushEvent }, async () => {
calls += 1;
return new Response(null, { status: 204 });
});
expect(calls).toBe(0);
});
test("does nothing when Discord output is disabled", async () => {
let calls = 0;
await notifyDiscord(parseDiscordConfig({ webhookUrl: "https://discord.example/webhook", notifyEvents: "push" }), { event: pushEvent }, async () => {
calls += 1;
return new Response(null, { status: 204 });
});
@ -101,7 +118,7 @@ describe("discord notifications", () => {
test("skips unconfigured events", async () => {
let calls = 0;
await notifyDiscord(parseDiscordConfig({ webhookUrl: "https://discord.example/webhook", notifyEvents: "release" }), { event: pushEvent }, async () => {
await notifyDiscord(parseDiscordConfig({ enabled: "true", webhookUrl: "https://discord.example/webhook", notifyEvents: "release" }), { event: pushEvent }, async () => {
calls += 1;
return new Response(null, { status: 204 });
});
@ -110,7 +127,7 @@ describe("discord notifications", () => {
test("posts configured events", async () => {
let body = "";
await notifyDiscord(parseDiscordConfig({ webhookUrl: "https://discord.example/webhook", notifyEvents: "push" }), { event: pushEvent }, async (_url, init) => {
await notifyDiscord(parseDiscordConfig({ enabled: "true", webhookUrl: "https://discord.example/webhook", notifyEvents: "push" }), { event: pushEvent }, async (_url, init) => {
body = String(init?.body);
return new Response(null, { status: 204 });
});
@ -119,7 +136,7 @@ describe("discord notifications", () => {
});
test("throws on Discord failure", async () => {
await expect(notifyDiscord(parseDiscordConfig({ webhookUrl: "https://discord.example/webhook" }), { event: pushEvent }, async () => {
await expect(notifyDiscord(parseDiscordConfig({ enabled: "true", webhookUrl: "https://discord.example/webhook" }), { event: pushEvent }, async () => {
return new Response("bad", { status: 500 });
})).rejects.toThrow("Discord webhook returned 500");
});

View file

@ -95,7 +95,7 @@ describe("feed watcher", () => {
const sourcesPath = join(dataDir, "sources.json");
await writeFile(sourcesPath, JSON.stringify({ sources: [source] }), "utf8");
await pollFeedsOnce({ dataDir, sourcesPath, discord: { webhookUrl: "https://discord.example/webhook", notifyEvents: new Set(["push"]) } }, async () => {
await pollFeedsOnce({ dataDir, sourcesPath, discord: { enabled: true, webhookUrl: "https://discord.example/webhook", notifyEvents: new Set(["push"]) } }, async () => {
return new Response(atom, { status: 200 });
});
@ -128,7 +128,7 @@ describe("feed watcher", () => {
}), "utf8");
let feedCalls = 0;
await pollFeedsOnce({ dataDir, sourcesPath, discord: { notifyEvents: new Set(["release"]) } }, async () => {
await pollFeedsOnce({ dataDir, sourcesPath, discord: { enabled: false, notifyEvents: new Set(["release"]) } }, async () => {
feedCalls += 1;
return new Response(rss, { status: 200 });
});
@ -170,7 +170,7 @@ describe("feed watcher", () => {
await pollFeedsOnce({
dataDir,
sourcesPath,
discord: { notifyEvents: new Set(["release"]) },
discord: { enabled: false, notifyEvents: new Set(["release"]) },
flowDispatch: {
env: {
FLOW_URL: "https://flow.example/events",

View file

@ -78,6 +78,7 @@ describe("server", () => {
jojoSecret: "jojo",
dataDir,
discord: {
enabled: true,
webhookUrl: "https://discord.example/webhook",
notifyEvents: new Set(["push"]),
},