// Meldungen nach Discord. // // WOZU. Die Sicherung laeuft nachts um halb fuenf, und wenn sie aufhoert zu // laufen, merkt es niemand -- das Panel zeigt es nur dem, der hinschaut. Genau // dafuer ist das hier: nicht fuer Jubelmeldungen, sondern fuer die zwei, drei // Faelle, in denen jemand etwas tun muss. // // Ein WEBHOOK und keine Bot-Anbindung: eine Adresse, kein Token, keine // Berechtigungen, kein zweites Geheimnis, das ablaeuft. Wer ihn zurueckziehen // will, loescht ihn in Discord. // // DER AUSSEHEN FOLGT DEM d4rkbot. Dessen src/embeds.js hat eine zentrale // Embed-Fabrik: Markenfarbe, Fusszeile "D4RKST3R // ", Zeitstempel. Zwei // Dienste desselben Hauses sollen in einem Kanal nicht wie zwei Fremde // aussehen, also uebernehmen wir das Muster statt ein eigenes zu erfinden. // // EINE ABWEICHUNG, mit Absicht: der Bot faerbt alles in der Markenfarbe, hier // faerbt die SCHWERE. Eine Warnung, die aussieht wie jede andere Nachricht, // ist eine Warnung, die man ueberliest -- und diese Meldungen gibt es nur, // weil jemand sie sehen soll. import { getSetting, setSetting } from './settings.js' import { now } from './db.js' export const MELDUNG_KEYS = { webhook: 'discord.webhook', /** Was gemeldet wird: Komma-Liste aus 'sicherung', 'verwaiste'. */ anlaesse: 'discord.anlaesse', /** Womit zuletzt gemeldet wurde -- gegen Wiederholungen. */ zuletzt: 'discord.zuletzt', /** Der Name in der Fusszeile. Wie beim Bot einstellbar, mit demselben * Standard. */ marke: 'marke.name', } as const /** Dieselben Werte wie im d4rkbot (src/runtime-settings.js). */ const MARKE_STANDARD = 'D4RKST3R' const FARBEN = { marke: 0xf5c518, gut: 0x3fb950, warnung: 0xd29922, schlecht: 0xf85149, } as const export type Schwere = keyof typeof FARBEN export type MeldungConfig = { hatWebhook: boolean anlaesse: string[] } export function meldungConfig(): MeldungConfig { const roh = getSetting(MELDUNG_KEYS.anlaesse) return { hatWebhook: Boolean(getSetting(MELDUNG_KEYS.webhook)), // Ohne Eintrag gilt: alles, was es gibt. Wer einen Webhook eintraegt, // will Meldungen -- sonst haette er es gelassen. anlaesse: roh ? roh.split(',').map((a) => a.trim()).filter(Boolean) : ['sicherung', 'verwaiste'], } } type Feld = { name: string; value: string; inline?: boolean } /** Eine Nachricht schicken. Wirft nie. * * `einmalig` ist der Schluessel gegen Wiederholungen: dieselbe Zeichenkette * wird nur einmal gemeldet, bis sich der Zustand aendert. */ export async function melde( titel: string, opt: { text?: string felder?: Feld[] schwere?: Schwere anlass?: string einmalig?: string tag?: string } = {}, ): Promise { try { const webhook = getSetting(MELDUNG_KEYS.webhook) if (!webhook) return false if (opt.anlass && !meldungConfig().anlaesse.includes(opt.anlass)) return false if (opt.einmalig) { const zuletzt = getSetting(MELDUNG_KEYS.zuletzt) if (zuletzt === opt.einmalig) return false setSetting(MELDUNG_KEYS.zuletzt, opt.einmalig) } const marke = getSetting(MELDUNG_KEYS.marke) || MARKE_STANDARD const antwort = await fetch(webhook, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username: 'd4rk_media', embeds: [ { title: titel, description: opt.text, color: FARBEN[opt.schwere ?? 'marke'], // Felder statt Fliesstext, wo es Zahlen sind: "228 // Dateien / 11,5 MB / vor 3 Stunden" liest sich in // einer Zeile schlechter als nebeneinander. fields: opt.felder, footer: { text: `${marke} // ${opt.tag ?? 'MEDIA'}` }, timestamp: new Date(now()).toISOString(), }, ], }), }) // Discord antwortet auf einen Webhook mit 204. if (!antwort.ok) { console.error('[discord] Webhook antwortete mit', antwort.status) return false } return true } catch (err) { // Eine Meldung, die den Dienst mitreisst, waere die Buchhaltung, die // das Geschaeft verhindert. console.error('[discord] Meldung fehlgeschlagen:', err) return false } } const groesse = (n: number) => n < 1024 * 1024 ? `${Math.round(n / 1024)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB` /** Wacht ueber die Sicherung. * * Der eigentliche Punkt der ganzen Datei. Geprueft wird stuendlich, gemeldet * hoechstens einmal je Zustand: * * - der letzte Lauf ist gescheitert * - der letzte Lauf ist laenger als 26 Stunden her (taeglich um 04:30 plus * Luft; 24 waeren zu knapp, 48 zu spaet) * - es gab noch nie einen */ export async function pruefeSicherung(): Promise { const roh = getSetting('sicherung.letzte') if (!roh) { await melde('Noch keine Sicherung gemeldet', { text: 'Es wurde noch **nie** eine Sicherung eingetragen. Läuft die geplante Aufgabe?', schwere: 'warnung', anlass: 'sicherung', einmalig: 'nie', tag: 'SICHERUNG', }) return } try { const letzte = JSON.parse(roh) as { at: number ok: boolean dateien?: number groesse?: number hoch?: boolean fehler?: string } const stunden = (now() - letzte.at) / 3_600_000 const wann = `` if (!letzte.ok) { await melde('Die Sicherung ist fehlgeschlagen', { text: letzte.fehler ?? 'Es wurde kein Grund gemeldet.', felder: [ { name: 'Wann', value: wann, inline: true }, { name: 'Zuletzt gut', value: letzte.dateien ? `${letzte.dateien} Dateien` : '—', inline: true, }, ], schwere: 'schlecht', anlass: 'sicherung', einmalig: `fehler:${letzte.at}`, tag: 'SICHERUNG', }) } else if (stunden > 26) { await melde('Die Sicherung ist überfällig', { text: 'Läuft die geplante Aufgabe noch? Erwartet wird sie täglich um 04:30.', felder: [ { name: 'Letzter Lauf', value: wann, inline: true }, { name: 'Das ist', value: `${Math.floor(stunden)} Stunden her`, inline: true }, ], schwere: 'warnung', anlass: 'sicherung', einmalig: `alt:${letzte.at}`, tag: 'SICHERUNG', }) } } catch { /* kaputter Eintrag -- beim naechsten Lauf wieder */ } } /** Die Testnachricht. Zeigt bewusst, WIE eine echte aussieht — sonst prueft * man nur, ob irgendetwas ankommt, und nicht, ob es lesbar ist. */ export async function testMeldung(wer: string, bestand: { files: number; bytes: number }) { return melde('Testnachricht', { text: `Ausgelöst von **${wer}**. So sieht eine Meldung von hier aus.`, felder: [ { name: 'Bestand', value: `${bestand.files} Dateien`, inline: true }, { name: 'Belegt', value: groesse(bestand.bytes), inline: true }, ], schwere: 'gut', }) }