diff --git a/lib/config-store.js b/lib/config-store.js index 4f2e048..d6f372a 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -60,6 +60,7 @@ const DEFAULTS = { sessionLog: false, // legacy boolean (kept for back-compat reads); normalized into logMode on load logVerbose: false, // when true, [DEBUG] level entries are written to debug.log webhookUrl: '', // POST target on batch-done (Discord or generic JSON) + webhookMention: '', // optional Discord ping target: user-id, role:id, @here, @everyone autoRetryRounds: 0, // 0 = off; 1-5 automatic retry rounds for transient failures after batch end autoRetryDelayMin: 5, // base delay in minutes between auto-retry rounds (linear backoff: round N waits N*delay) // NOTE: logMode is intentionally NOT in DEFAULTS. If it were, the deep-merge diff --git a/lib/webhook-notify.js b/lib/webhook-notify.js index 5a098a8..ea26507 100644 --- a/lib/webhook-notify.js +++ b/lib/webhook-notify.js @@ -27,6 +27,26 @@ function summarizePerHosterFromBatch(summary) { return out; } +function resolveDiscordMention(raw) { + const s = String(raw || '').trim(); + if (!s) return null; + const keyword = s.replace(/^@/, '').toLowerCase(); + if (keyword === 'here' || keyword === 'everyone') { + return { token: `@${keyword}`, allowed: { parse: ['everyone'] } }; + } + const roleMatch = s.match(/^(?:<@&(\d+)>|role:(\d+))$/i); + if (roleMatch) { + const id = roleMatch[1] || roleMatch[2]; + return { token: `<@&${id}>`, allowed: { roles: [id] } }; + } + const userMatch = s.match(/^(?:<@!?(\d+)>|user:(\d+)|(\d{5,30}))$/i); + if (userMatch) { + const id = userMatch[1] || userMatch[2] || userMatch[3]; + return { token: `<@${id}>`, allowed: { users: [id] } }; + } + return null; +} + function buildWebhookRequest(url, summary, meta) { const m = meta || {}; const total = Number(summary && summary.total) || 0; @@ -45,7 +65,10 @@ function buildWebhookRequest(url, summary, meta) { `✅ ${succeeded} ok · ❌ ${failed} Fehler · 📦 ${total} gesamt · ⏱ ${duration}` ]; if (hosterLines) lines.push(hosterLines); - body = JSON.stringify({ content: lines.join('\n') }); + const mention = resolveDiscordMention(m.mention); + const payload = { content: (mention ? mention.token + ' ' : '') + lines.join('\n') }; + payload.allowed_mentions = mention ? mention.allowed : { parse: [] }; + body = JSON.stringify(payload); } else { body = JSON.stringify({ event: 'batch-done', @@ -69,4 +92,4 @@ function buildWebhookRequest(url, summary, meta) { }; } -module.exports = { isDiscordWebhook, formatDurationShort, summarizePerHosterFromBatch, buildWebhookRequest }; +module.exports = { isDiscordWebhook, formatDurationShort, summarizePerHosterFromBatch, buildWebhookRequest, resolveDiscordMention }; diff --git a/main.js b/main.js index 00ec21a..475e973 100644 --- a/main.js +++ b/main.js @@ -289,6 +289,7 @@ function sendBatchWebhook(summary, durationSec) { durationSec, appVersion: app.getVersion(), machineName: require('os').hostname(), + mention: gs.webhookMention || '', timestamp: new Date().toISOString() }); fetch(req.url, { @@ -1876,8 +1877,9 @@ ipcMain.handle('reveal-log-file', async (_event, target) => { } }); -ipcMain.handle('test-webhook', async (_event, url) => { - const target = String(url || '').trim(); +ipcMain.handle('test-webhook', async (_event, payload) => { + const target = (typeof payload === 'string' ? payload : (payload && payload.url) || '').trim(); + const mention = (payload && typeof payload === 'object' && payload.mention) || ''; if (!target || !/^https?:\/\//i.test(target)) return { ok: false, error: 'Ungültige URL (muss mit http(s):// beginnen)' }; try { const req = buildWebhookRequest(target, { @@ -1891,6 +1893,7 @@ ipcMain.handle('test-webhook', async (_event, url) => { durationSec: 754, appVersion: app.getVersion(), machineName: require('os').hostname(), + mention, timestamp: new Date().toISOString() }); const res = await fetch(req.url, { diff --git a/preload.js b/preload.js index 5b215a4..142bb25 100644 --- a/preload.js +++ b/preload.js @@ -119,7 +119,7 @@ contextBridge.exposeInMainWorld('api', { resetSessionFailedAccount: (payload) => ipcRenderer.invoke('reset-session-failed-account', payload), resetAllSessionFailedAccounts: () => ipcRenderer.invoke('reset-all-session-failed-accounts'), getLogPaths: () => ipcRenderer.invoke('get-log-paths'), - testWebhook: (url) => ipcRenderer.invoke('test-webhook', url), + testWebhook: (payload) => ipcRenderer.invoke('test-webhook', payload), onNetworkStatus: (callback) => { ipcRenderer.on('network-status', (_event, data) => callback(data)); }, diff --git a/renderer/app.js b/renderer/app.js index 81ae2dc..7f9a901 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -2841,6 +2841,11 @@ function renderSettings() { Bei Batch-Ende wird eine Zusammenfassung gepostet (Discord wird automatisch erkannt, sonst generisches JSON). +