Feature: Discord-Ping in Benachrichtigungen (optionale Erwaehnung)
Neues Settings-Feld "Discord-Ping" unter der Webhook-URL: User-ID, @everyone oder @here. Wird jeder Webhook-Nachricht vorangestellt, damit Discord wirklich pingt statt nur still in den Kanal zu schreiben. Eine nackte Zahl wird als <@id> verpackt (nur so pingt eine User-Erwaehnung); @everyone/@here und fertige <@...>-Mentions gehen unveraendert durch. Default leer = Verhalten wie bisher. 5 neue Tests (Normalisierung + Content-Prefix).
This commit is contained in:
parent
f08db49f0b
commit
8ab87bda51
@ -108,6 +108,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
confirmDeleteSelection: true,
|
confirmDeleteSelection: true,
|
||||||
backupIncludeDownloads: false,
|
backupIncludeDownloads: false,
|
||||||
notifyUrl: "",
|
notifyUrl: "",
|
||||||
|
notifyMention: "",
|
||||||
notifyOnPackageCompleted: false,
|
notifyOnPackageCompleted: false,
|
||||||
notifyOnPackageFailed: false,
|
notifyOnPackageFailed: false,
|
||||||
notifyOnRunFinished: false,
|
notifyOnRunFinished: false,
|
||||||
|
|||||||
@ -10497,7 +10497,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.notifiedPackages.add(pkg.id);
|
this.notifiedPackages.add(pkg.id);
|
||||||
void sendNotification(url, {
|
void sendNotification(url, {
|
||||||
title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen",
|
title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen",
|
||||||
message: `${pkg.name}\n${detail}`
|
message: `${pkg.name}\n${detail}`,
|
||||||
|
mention: this.settings.notifyMention
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12121,7 +12122,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (this.settings.notifyOnRunFinished && total > 0) {
|
if (this.settings.notifyOnRunFinished && total > 0) {
|
||||||
void sendNotification(this.settings.notifyUrl, {
|
void sendNotification(this.settings.notifyUrl, {
|
||||||
title: failed > 0 ? "⚠️ Durchlauf beendet" : "🏁 Durchlauf beendet",
|
title: failed > 0 ? "⚠️ Durchlauf beendet" : "🏁 Durchlauf beendet",
|
||||||
message: `${success}/${total} erfolgreich, ${failed} fehlgeschlagen, ${cancelled} abgebrochen\nDauer ${duration}s, Durchschnitt ${humanSize(avgSpeed)}/s`
|
message: `${success}/${total} erfolgreich, ${failed} fehlgeschlagen, ${cancelled} abgebrochen\nDauer ${duration}s, Durchschnitt ${humanSize(avgSpeed)}/s`,
|
||||||
|
mention: this.settings.notifyMention
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.runItemIds.clear();
|
this.runItemIds.clear();
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { logger } from "./logger";
|
|||||||
export interface NotifyPayload {
|
export interface NotifyPayload {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
mention?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFY_TIMEOUT_MS = 5000;
|
const NOTIFY_TIMEOUT_MS = 5000;
|
||||||
@ -12,8 +13,22 @@ export function isNotifyUrlValid(url: string): boolean {
|
|||||||
return /^https?:\/\/\S+$/i.test(String(url || "").trim());
|
return /^https?:\/\/\S+$/i.test(String(url || "").trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accepts a bare Discord user ID (wrapped as <@id> so it actually pings),
|
||||||
|
// @everyone/@here, or an already-formed <@...>/<@&...> mention as-is.
|
||||||
|
export function normalizeDiscordMention(raw: string): string {
|
||||||
|
const text = String(raw || "").trim();
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (/^\d{5,}$/.test(text)) {
|
||||||
|
return `<@${text}>`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildNotifyRequest(url: string, payload: NotifyPayload): { url: string; init: RequestInit } {
|
export function buildNotifyRequest(url: string, payload: NotifyPayload): { url: string; init: RequestInit } {
|
||||||
const content = `**${payload.title}**\n${payload.message}`.slice(0, 2000);
|
const mention = normalizeDiscordMention(payload.mention || "");
|
||||||
|
const content = `${mention ? `${mention} ` : ""}**${payload.title}**\n${payload.message}`.slice(0, 2000);
|
||||||
return {
|
return {
|
||||||
url: String(url || "").trim(),
|
url: String(url || "").trim(),
|
||||||
init: {
|
init: {
|
||||||
|
|||||||
@ -460,6 +460,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
|
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
|
||||||
backupIncludeDownloads: settings.backupIncludeDownloads !== undefined ? Boolean(settings.backupIncludeDownloads) : defaults.backupIncludeDownloads,
|
backupIncludeDownloads: settings.backupIncludeDownloads !== undefined ? Boolean(settings.backupIncludeDownloads) : defaults.backupIncludeDownloads,
|
||||||
notifyUrl: asText(settings.notifyUrl) || defaults.notifyUrl,
|
notifyUrl: asText(settings.notifyUrl) || defaults.notifyUrl,
|
||||||
|
notifyMention: asText(settings.notifyMention) || defaults.notifyMention,
|
||||||
notifyOnPackageCompleted: settings.notifyOnPackageCompleted !== undefined ? Boolean(settings.notifyOnPackageCompleted) : defaults.notifyOnPackageCompleted,
|
notifyOnPackageCompleted: settings.notifyOnPackageCompleted !== undefined ? Boolean(settings.notifyOnPackageCompleted) : defaults.notifyOnPackageCompleted,
|
||||||
notifyOnPackageFailed: settings.notifyOnPackageFailed !== undefined ? Boolean(settings.notifyOnPackageFailed) : defaults.notifyOnPackageFailed,
|
notifyOnPackageFailed: settings.notifyOnPackageFailed !== undefined ? Boolean(settings.notifyOnPackageFailed) : defaults.notifyOnPackageFailed,
|
||||||
notifyOnRunFinished: settings.notifyOnRunFinished !== undefined ? Boolean(settings.notifyOnRunFinished) : defaults.notifyOnRunFinished,
|
notifyOnRunFinished: settings.notifyOnRunFinished !== undefined ? Boolean(settings.notifyOnRunFinished) : defaults.notifyOnRunFinished,
|
||||||
|
|||||||
@ -853,7 +853,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
|||||||
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
||||||
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
||||||
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false,
|
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false,
|
||||||
notifyUrl: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
|
notifyUrl: "", notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
|
||||||
accountListShowDetailedDebridLinkKeys: false,
|
accountListShowDetailedDebridLinkKeys: false,
|
||||||
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
|
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
|
||||||
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
|
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
|
||||||
@ -4960,6 +4960,9 @@ export function App(): ReactElement {
|
|||||||
<label>Webhook-URL (Discord)</label>
|
<label>Webhook-URL (Discord)</label>
|
||||||
<input value={settingsDraft.notifyUrl} placeholder="https://discord.com/api/webhooks/..." onChange={(e) => setText("notifyUrl", e.target.value)} />
|
<input value={settingsDraft.notifyUrl} placeholder="https://discord.com/api/webhooks/..." onChange={(e) => setText("notifyUrl", e.target.value)} />
|
||||||
<div className="hint">In Discord: Servereinstellungen → Integrationen → Webhooks → Neuer Webhook → URL kopieren und hier eintragen. Die gewählten Ereignisse landen als Nachricht im Kanal.</div>
|
<div className="hint">In Discord: Servereinstellungen → Integrationen → Webhooks → Neuer Webhook → URL kopieren und hier eintragen. Die gewählten Ereignisse landen als Nachricht im Kanal.</div>
|
||||||
|
<label>Discord-Ping (optional)</label>
|
||||||
|
<input value={settingsDraft.notifyMention} placeholder="Deine User-ID, @everyone oder @here" onChange={(e) => setText("notifyMention", e.target.value)} />
|
||||||
|
<div className="hint">Wird jeder Nachricht vorangestellt, damit Discord dich pingt. Eigene ID: Discord-Einstellungen → Erweitert → Entwicklermodus an, dann Rechtsklick auf deinen Namen → "User-ID kopieren" und die Zahl hier eintragen.</div>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.notifyOnPackageCompleted} onChange={(e) => setBool("notifyOnPackageCompleted", e.target.checked)} /> Benachrichtigen wenn ein Paket fertig ist</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.notifyOnPackageCompleted} onChange={(e) => setBool("notifyOnPackageCompleted", e.target.checked)} /> Benachrichtigen wenn ein Paket fertig ist</label>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.notifyOnPackageFailed} onChange={(e) => setBool("notifyOnPackageFailed", e.target.checked)} /> Benachrichtigen wenn ein Paket fehlschlägt</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.notifyOnPackageFailed} onChange={(e) => setBool("notifyOnPackageFailed", e.target.checked)} /> Benachrichtigen wenn ein Paket fehlschlägt</label>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.notifyOnRunFinished} onChange={(e) => setBool("notifyOnRunFinished", e.target.checked)} /> Benachrichtigen wenn der Durchlauf beendet ist</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.notifyOnRunFinished} onChange={(e) => setBool("notifyOnRunFinished", e.target.checked)} /> Benachrichtigen wenn der Durchlauf beendet ist</label>
|
||||||
|
|||||||
@ -133,6 +133,7 @@ export interface AppSettings {
|
|||||||
confirmDeleteSelection: boolean;
|
confirmDeleteSelection: boolean;
|
||||||
backupIncludeDownloads: boolean;
|
backupIncludeDownloads: boolean;
|
||||||
notifyUrl: string;
|
notifyUrl: string;
|
||||||
|
notifyMention: string;
|
||||||
notifyOnPackageCompleted: boolean;
|
notifyOnPackageCompleted: boolean;
|
||||||
notifyOnPackageFailed: boolean;
|
notifyOnPackageFailed: boolean;
|
||||||
notifyOnRunFinished: boolean;
|
notifyOnRunFinished: boolean;
|
||||||
|
|||||||
@ -1,5 +1,22 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { buildNotifyRequest, isNotifyUrlValid, sendNotification } from "../src/main/notify";
|
import { buildNotifyRequest, isNotifyUrlValid, normalizeDiscordMention, sendNotification } from "../src/main/notify";
|
||||||
|
|
||||||
|
describe("normalizeDiscordMention", () => {
|
||||||
|
it("wraps a bare user ID as a pinging mention", () => {
|
||||||
|
expect(normalizeDiscordMention("123456789012345678")).toBe("<@123456789012345678>");
|
||||||
|
expect(normalizeDiscordMention(" 987654321 ")).toBe("<@987654321>");
|
||||||
|
});
|
||||||
|
it("passes @everyone/@here and formed mentions through", () => {
|
||||||
|
expect(normalizeDiscordMention("@everyone")).toBe("@everyone");
|
||||||
|
expect(normalizeDiscordMention("@here")).toBe("@here");
|
||||||
|
expect(normalizeDiscordMention("<@123456789>")).toBe("<@123456789>");
|
||||||
|
expect(normalizeDiscordMention("<@&111222333>")).toBe("<@&111222333>");
|
||||||
|
});
|
||||||
|
it("returns empty for empty input", () => {
|
||||||
|
expect(normalizeDiscordMention("")).toBe("");
|
||||||
|
expect(normalizeDiscordMention(" ")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("isNotifyUrlValid", () => {
|
describe("isNotifyUrlValid", () => {
|
||||||
it("accepts http/https URLs", () => {
|
it("accepts http/https URLs", () => {
|
||||||
@ -30,6 +47,16 @@ describe("buildNotifyRequest", () => {
|
|||||||
const body = JSON.parse(String(req.init.body));
|
const body = JSON.parse(String(req.init.body));
|
||||||
expect(body.content.length).toBe(2000);
|
expect(body.content.length).toBe(2000);
|
||||||
});
|
});
|
||||||
|
it("prepends the mention so Discord pings (bare ID gets wrapped)", () => {
|
||||||
|
const req = buildNotifyRequest("https://discord.com/api/webhooks/123/abc", { title: "T", message: "M", mention: "123456789012345678" });
|
||||||
|
const body = JSON.parse(String(req.init.body));
|
||||||
|
expect(body.content).toBe("<@123456789012345678> **T**\nM");
|
||||||
|
});
|
||||||
|
it("sends no mention prefix when the field is empty", () => {
|
||||||
|
const req = buildNotifyRequest("https://discord.com/api/webhooks/123/abc", { title: "T", message: "M", mention: "" });
|
||||||
|
const body = JSON.parse(String(req.init.body));
|
||||||
|
expect(body.content).toBe("**T**\nM");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sendNotification", () => {
|
describe("sendNotification", () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user