diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 34fbb77..7c9128f 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -10496,10 +10496,8 @@ export class DownloadManager extends EventEmitter { } this.notifiedPackages.add(pkg.id); void sendNotification(url, { - title: kind === "completed" ? "Paket fertig" : "Paket fehlgeschlagen", - message: `${pkg.name}\n${detail}`, - priority: kind === "failed" ? "high" : "default", - tags: kind === "completed" ? "white_check_mark" : "x" + title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen", + message: `${pkg.name}\n${detail}` }); } @@ -12122,10 +12120,8 @@ export class DownloadManager extends EventEmitter { this.session.summaryText = `Summary: Dauer ${duration}s, Ø Speed ${humanSize(avgSpeed)}/s, Erfolg ${success}/${total}`; if (this.settings.notifyOnRunFinished && total > 0) { void sendNotification(this.settings.notifyUrl, { - title: "Durchlauf beendet", - message: `${success}/${total} erfolgreich, ${failed} fehlgeschlagen, ${cancelled} abgebrochen\nDauer ${duration}s, Durchschnitt ${humanSize(avgSpeed)}/s`, - priority: failed > 0 ? "high" : "default", - tags: failed > 0 ? "warning" : "checkered_flag" + title: failed > 0 ? "⚠️ Durchlauf beendet" : "🏁 Durchlauf beendet", + message: `${success}/${total} erfolgreich, ${failed} fehlgeschlagen, ${cancelled} abgebrochen\nDauer ${duration}s, Durchschnitt ${humanSize(avgSpeed)}/s` }); } this.runItemIds.clear(); diff --git a/src/main/notify.ts b/src/main/notify.ts index 44b9bd2..e9fc5a1 100644 --- a/src/main/notify.ts +++ b/src/main/notify.ts @@ -3,30 +3,24 @@ import { logger } from "./logger"; export interface NotifyPayload { title: string; message: string; - priority?: "default" | "high"; - tags?: string; } const NOTIFY_TIMEOUT_MS = 5000; +const WEBHOOK_USERNAME = "Real-Debrid Downloader"; export function isNotifyUrlValid(url: string): boolean { return /^https?:\/\/\S+$/i.test(String(url || "").trim()); } export function buildNotifyRequest(url: string, payload: NotifyPayload): { url: string; init: RequestInit } { - const headers: Record = { - "Title": payload.title, - "Content-Type": "text/plain; charset=utf-8" - }; - if (payload.priority && payload.priority !== "default") { - headers["Priority"] = payload.priority; - } - if (payload.tags) { - headers["Tags"] = payload.tags; - } + const content = `**${payload.title}**\n${payload.message}`.slice(0, 2000); return { url: String(url || "").trim(), - init: { method: "POST", headers, body: payload.message } + init: { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: WEBHOOK_USERNAME, content }) + } }; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b0a22b1..7da08be 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -4957,9 +4957,9 @@ export function App(): ReactElement { - - setText("notifyUrl", e.target.value)} /> -
POST an diese URL bei den unten gewählten Ereignissen. Mit der ntfy-App aufs Handy: Topic-URL eintragen, Topic in der App abonnieren — kein Account nötig.
+ + setText("notifyUrl", e.target.value)} /> +
In Discord: Servereinstellungen → Integrationen → Webhooks → Neuer Webhook → URL kopieren und hier eintragen. Die gewählten Ereignisse landen als Nachricht im Kanal.
diff --git a/tests/notify.test.ts b/tests/notify.test.ts index e98d28d..73f6724 100644 --- a/tests/notify.test.ts +++ b/tests/notify.test.ts @@ -3,46 +3,48 @@ import { buildNotifyRequest, isNotifyUrlValid, sendNotification } from "../src/m describe("isNotifyUrlValid", () => { it("accepts http/https URLs", () => { - expect(isNotifyUrlValid("https://ntfy.sh/mein-topic")).toBe(true); + expect(isNotifyUrlValid("https://discord.com/api/webhooks/123/abc")).toBe(true); expect(isNotifyUrlValid("http://192.168.1.10:8080/hook")).toBe(true); - expect(isNotifyUrlValid(" https://ntfy.sh/topic ")).toBe(true); + expect(isNotifyUrlValid(" https://discord.com/api/webhooks/123/abc ")).toBe(true); }); it("rejects empty and non-http values", () => { expect(isNotifyUrlValid("")).toBe(false); - expect(isNotifyUrlValid("ntfy.sh/topic")).toBe(false); + expect(isNotifyUrlValid("discord.com/api/webhooks/123/abc")).toBe(false); expect(isNotifyUrlValid("ftp://x")).toBe(false); expect(isNotifyUrlValid("https:// mit leerzeichen")).toBe(false); }); }); describe("buildNotifyRequest", () => { - it("builds an ntfy-style POST with title/priority/tags headers and message body", () => { - const req = buildNotifyRequest(" https://ntfy.sh/topic ", { title: "Paket fertig", message: "Show.S01\n5 Datei(en)", priority: "high", tags: "x" }); - expect(req.url).toBe("https://ntfy.sh/topic"); + it("builds a Discord-compatible JSON webhook POST (bold title + message as content)", () => { + const req = buildNotifyRequest(" https://discord.com/api/webhooks/123/abc ", { title: "✅ Paket fertig", message: "Show.S01\n5 Datei(en)" }); + expect(req.url).toBe("https://discord.com/api/webhooks/123/abc"); expect(req.init.method).toBe("POST"); - expect(req.init.body).toBe("Show.S01\n5 Datei(en)"); - expect(req.init.headers).toMatchObject({ Title: "Paket fertig", Priority: "high", Tags: "x" }); + expect(req.init.headers).toMatchObject({ "Content-Type": "application/json" }); + const body = JSON.parse(String(req.init.body)); + expect(body.content).toBe("**✅ Paket fertig**\nShow.S01\n5 Datei(en)"); + expect(body.username).toBe("Real-Debrid Downloader"); }); - it("omits default priority and empty tags", () => { - const req = buildNotifyRequest("https://ntfy.sh/topic", { title: "T", message: "M", priority: "default" }); - expect(req.init.headers).not.toHaveProperty("Priority"); - expect(req.init.headers).not.toHaveProperty("Tags"); + it("caps the content at Discord's 2000-char limit", () => { + const req = buildNotifyRequest("https://discord.com/api/webhooks/123/abc", { title: "T", message: "x".repeat(3000) }); + const body = JSON.parse(String(req.init.body)); + expect(body.content.length).toBe(2000); }); }); describe("sendNotification", () => { - it("returns true on HTTP ok", async () => { - const fetchFn = vi.fn().mockResolvedValue(new Response("", { status: 200 })); - await expect(sendNotification("https://ntfy.sh/topic", { title: "T", message: "M" }, fetchFn)).resolves.toBe(true); + it("returns true on HTTP ok (Discord answers 204 No Content)", async () => { + const fetchFn = vi.fn().mockResolvedValue(new Response(null, { status: 204 })); + await expect(sendNotification("https://discord.com/api/webhooks/123/abc", { title: "T", message: "M" }, fetchFn)).resolves.toBe(true); expect(fetchFn).toHaveBeenCalledTimes(1); }); it("returns false on HTTP error without throwing", async () => { const fetchFn = vi.fn().mockResolvedValue(new Response("", { status: 500 })); - await expect(sendNotification("https://ntfy.sh/topic", { title: "T", message: "M" }, fetchFn)).resolves.toBe(false); + await expect(sendNotification("https://discord.com/api/webhooks/123/abc", { title: "T", message: "M" }, fetchFn)).resolves.toBe(false); }); it("returns false on network error without throwing", async () => { const fetchFn = vi.fn().mockRejectedValue(new Error("offline")); - await expect(sendNotification("https://ntfy.sh/topic", { title: "T", message: "M" }, fetchFn)).resolves.toBe(false); + await expect(sendNotification("https://discord.com/api/webhooks/123/abc", { title: "T", message: "M" }, fetchFn)).resolves.toBe(false); }); it("does not call fetch for an invalid URL", async () => { const fetchFn = vi.fn();