// Benachrichtigung aufs Handy (5.5.0, aus ARM: Apprise/Pushover/…) — zwei // Wege, beide ohne Bibliothek, beide dokumentiert (Regel D): // Discord-Webhook: POST mit JSON { content } — // discord.com/developers/docs/resources/webhook („Execute Webhook": // content ist eine Zeichenkette bis 2000 Zeichen). // Telegram-Bot: POST https://api.telegram.org/bot/sendMessage mit // JSON { chat_id, text } — core.telegram.org/bots/api#sendmessage. // Ein Fehlschlag wird GESAGT (Ereignis-Zeile), kippt aber nie den Vorgang, // der ihn ausgelöst hat. import type { KernKontext } from '../kontext' import type { EinstellungsDienst } from './einstellungen' export interface Kanaele { discordWebhook: string telegramToken: string telegramChatId: string } /** Was an Discord geht (pur, testbar). */ export function discordAnfrage(webhook: string, titel: string, text: string): { url: string; body: string } { const inhalt = `**${titel}**\n${text}`.slice(0, 1990) return { url: webhook, body: JSON.stringify({ content: inhalt, username: 'Rippy' }) } } /** Was an Telegram geht (pur, testbar). */ export function telegramAnfrage(token: string, chatId: string, titel: string, text: string): { url: string; body: string } { return { url: `https://api.telegram.org/bot${token}/sendMessage`, body: JSON.stringify({ chat_id: chatId, text: `${titel}\n${text}`.slice(0, 4000) }), } } export function kanaeleAus(lesen: (schluessel: string) => string): Kanaele { return { discordWebhook: lesen('discordWebhook').trim(), telegramToken: lesen('telegramToken').trim(), telegramChatId: lesen('telegramChatId').trim(), } } export function kanaeleEingerichtet(k: Kanaele): boolean { return k.discordWebhook.length > 0 || (k.telegramToken.length > 0 && k.telegramChatId.length > 0) } export class BenachrichtigungsDienst { constructor( private readonly kontext: KernKontext, private readonly einstellungen: EinstellungsDienst, private readonly holen: typeof fetch = fetch, ) {} eingeschaltet(): boolean { return this.einstellungen.lesen('benachrichtigungen', 'an') !== 'aus' } /** Schickt über alle eingerichteten Kanäle. Liefert die Fehler (leer = alles gut). */ async senden(titel: string, text: string, erzwingen = false): Promise { if (!erzwingen && !this.eingeschaltet()) return [] const k = kanaeleAus((s) => this.einstellungen.lesen(s)) const fehler: string[] = [] const anfragen: Array<{ name: string; url: string; body: string }> = [] if (k.discordWebhook.length > 0) anfragen.push({ name: 'Discord', ...discordAnfrage(k.discordWebhook, titel, text) }) if (k.telegramToken.length > 0 && k.telegramChatId.length > 0) { anfragen.push({ name: 'Telegram', ...telegramAnfrage(k.telegramToken, k.telegramChatId, titel, text) }) } for (const a of anfragen) { try { const antwort = await this.holen(a.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: a.body, signal: AbortSignal.timeout(15_000), }) if (antwort.status >= 300) fehler.push(`${a.name}: HTTP ${antwort.status}`) } catch (e) { fehler.push(`${a.name}: ${String((e as Error).message ?? e).slice(0, 120)}`) } } for (const f of fehler) this.kontext.fehler(`Benachrichtigung ${f}`) if (anfragen.length > 0 && fehler.length === 0) this.kontext.protokoll.schreiben(`Benachrichtigt (${anfragen.map((a) => a.name).join(', ')}): ${titel}`) return fehler } /** Der Knopf „Testnachricht" — antwortet IMMER etwas. */ async testen(): Promise { const k = kanaeleAus((s) => this.einstellungen.lesen(s)) if (!kanaeleEingerichtet(k)) { this.kontext.fehler('Benachrichtigung: kein Kanal eingerichtet — Discord-Webhook oder Telegram-Bot eintragen.') return } const fehler = await this.senden('Rippy meldet sich', 'Testnachricht — die Verbindung steht.', true) if (fehler.length === 0) this.kontext.hinweis('Testnachricht verschickt.') } }