#!/usr/bin/env bash # Meldeweg der Box (Autonomie E1): eine Nachricht an den User schicken. # Primär: hermes send → Telegram (nutzt Gateway-Credentials, kein LLM nötig). # Zweitweg (seit 24.09.2026): direkt an die Telegram-Bot-API mit den Zugangsdaten aus Hermes' .env — # greift, wenn Hermes steht oder fehlt (die Homelab-Instanz hat gar kein Hermes). # Letzter Ausweg: Logfile + wall. # # Nutzung: notify.sh "Nachricht" (oder via stdin: echo msg | notify.sh) # notify.sh -s "[Update]" "Text" (Betreffzeile voranstellen) # notify.sh -d -s "[Update]" "…" (dringend: geht auch nachts sofort raus) # # Nachtruhe (User-Entscheid 24.09.2026): Zwischen 00:00 und 06:59 wird alles, was nicht dringend # ist, in der Nacht-Warteschlange gesammelt und um 07:00 von morgenmeldung.sh als EINE Meldung # verschickt. Dringend ist: -d, ein Betreff mit „Alarm“/„Notfall“ oder ein Text, der mit # „KRITISCH“ beginnt. (Bis 24.09. lieferte niemand die Warteschlange aus — seit dem 21.08. # gingen so alle Nachtmeldungen verloren, auch die Sonntags-Updates.) # # Exit 0 = zugestellt (Telegram, Fallback-Log oder Nacht-Warteschlange). # MC_NOTIFY_STRICT=1: Exit 1, wenn Telegram scheitert (für morgenmeldung.sh, damit nichts verloren geht). # Test-Schalter: MC_NOTIFY_STUNDE (Stunde vortäuschen), MC_NIGHT_QUEUE, MC_NOTIFY_HERMES (hermes-Befehl), # MC_TELEGRAM_ENV (Datei mit TELEGRAM_BOT_TOKEN/TELEGRAM_HOME_CHANNEL), MC_TELEGRAM_API (Bot-API-Adresse), # MC_NOTIFY_OHNE_WALL=1 (kein wall im letzten Ausweg). set -uo pipefail LOG="${MC_NOTIFY_LOG:-$HOME/mc2-notify.log}" NIGHT_QUEUE="${MC_NIGHT_QUEUE:-$HOME/.hermes/night-queue.txt}" HERMES_BIN="${MC_NOTIFY_HERMES:-hermes}" TG_ENV="${MC_TELEGRAM_ENV:-$HOME/.hermes/.env}" TG_API="${MC_TELEGRAM_API:-https://api.telegram.org}" SUBJECT="" DRINGEND=0 while getopts ":s:d" opt; do case "$opt" in s) SUBJECT="$OPTARG" ;; d) DRINGEND=1 ;; *) echo "usage: notify.sh [-d] [-s subject] " >&2; exit 2 ;; esac done shift $((OPTIND - 1)) MSG="${1:-}" [ -z "$MSG" ] && [ ! -t 0 ] && MSG="$(cat)" if [ -z "$MSG" ]; then # Bei leerer Pipe-Eingabe lautlos beenden (wichtig für Cron-Skripte ohne Ausgabe) exit 0 fi TS="$(date '+%Y-%m-%d %H:%M:%S')" # Lucy-Briefkasten (Proaktivität A3): Meldung zusätzlich an MC2 spiegeln — die Desktop-Lucy # spricht sie dann von sich aus. Best-effort (darf den Telegram-Weg nie aufhalten). # MC_NOTIFY_NO_ANNOUNCE=1 unterdrückt das (der Health-Wächter legt seine Einträge selbst ab). if [ "${MC_NOTIFY_NO_ANNOUNCE:-0}" != "1" ]; then curl -sf -m 3 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \ -H 'Content-Type: application/json' \ --data "$(python3 - "$SUBJECT" "$MSG" <<'PY' import json, sys print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1], "source": "notify"})) PY )" >/dev/null 2>&1 || true fi # Dringend ist, was nicht bis zum Morgen warten darf. case "${SUBJECT,,}" in *alarm*|*notfall*) DRINGEND=1 ;; esac case "$MSG" in KRITISCH*) DRINGEND=1 ;; esac # Lucys Stimme (User-Wunsch 25.09.2026: „Hermes' Persona ist Lucy — das muss sich in all ihren Meldungen # widerspiegeln“). Was über Telegram rausgeht, kommt von ihrem Bot, also spricht sie: Anrede „Commander“, knapp, # sachlich, ohne Emojis und Ausrufe (~/.hermes/SOUL.md). Die Kopfzeile folgt aus dem Betreff; beginnt die Nachricht # schon mit ihrer Anrede (Morgenmeldung, von Lucy selbst formulierte Berichte), bleibt sie weg. Melde-Log und # Auswertungen (update_verlauf.py, protokoll.py) behalten Betreff und Wortlaut wie bisher. lucy_kopf() { case "$MSG" in Commander*|"Guten Morgen, Commander"*) return 0 ;; esac case "${SUBJECT,,}" in *test*) echo "Commander, ein Test des Meldewegs." ; return 0 ;; *morgenmeldung*) echo "Guten Morgen, Commander." ; return 0 ;; esac if [ "$DRINGEND" = "1" ]; then echo "Commander, das ist dringend." return 0 fi case "${SUBJECT,,}" in *homelab-problem*) echo "Commander, im Homelab stimmt etwas nicht." ;; *"homelab wieder ok"*) echo "Commander, Entwarnung im Homelab." ;; *homelab-update*) echo "Commander, Bericht aus dem Homelab." ;; *box-problem*) echo "Commander, auf der Box stimmt etwas nicht." ;; *"box wieder ok"*) echo "Commander, Entwarnung auf der Box." ;; *update*) echo "Commander, Bericht von der Wartung." ;; *deploy*) echo "Commander, Bericht vom Deploy." ;; *erinnerung*) echo "Commander, eine Erinnerung." ;; *radar*) echo "Commander, Bericht vom Modell-Radar." ;; "") echo "Commander, kurz gesagt:" ;; *) echo "Commander, $(printf '%s' "$SUBJECT" | tr -d '[]')." ;; esac } # Dasselbe als kurze Marke für eine Zeile der Morgenmeldung („Homelab, Störung: …“ statt „[Homelab-Problem]: …“). lucy_marke() { case "${SUBJECT,,}" in "") return 0 ;; *alarm*|*notfall*) echo "Dringend" ;; *homelab-problem*) echo "Homelab, Störung" ;; *"homelab wieder ok"*) echo "Homelab, Entwarnung" ;; *homelab-update*) echo "Homelab, Update" ;; *box-problem*) echo "Box, Störung" ;; *"box wieder ok"*) echo "Box, Entwarnung" ;; *box-update*) echo "Box, Wartung" ;; *) printf '%s' "$SUBJECT" | tr -d '[]' ;; esac } KOPF="$(lucy_kopf)" # Nachtruhe: 00:00–06:59 sammeln, außer es ist dringend. 10#… verhindert, dass „08“/„09“ als # Oktalzahl gelesen werden. HOUR="${MC_NOTIFY_STUNDE:-$(date +%H)}" if [ "$DRINGEND" != "1" ] && [ "$((10#$HOUR))" -lt 7 ]; then mkdir -p "$(dirname "$NIGHT_QUEUE")" MARKE="$(lucy_marke)" printf -- '- %s%s\n' "${MARKE:+$MARKE: }" "$MSG" >> "$NIGHT_QUEUE" # Wortlaut „QUEUED für Morgen-Digest“ nicht ändern: services/update_verlauf.py liest ihn. echo "$TS QUEUED für Morgen-Digest: $MSG" >> "$LOG" exit 0 fi # Primärweg: Telegram via hermes send (Login-Shell-PATH, falls aus Timer/cron aufgerufen). Die Kopfzeile ist Lucys. if [ -n "$KOPF" ]; then SEND_OUT="$(bash -lc '"$1" send --to telegram --subject "$2" -- "$3"' _ "$HERMES_BIN" "$KOPF" "$MSG" 2>&1)" else SEND_OUT="$(bash -lc '"$1" send --to telegram -- "$2"' _ "$HERMES_BIN" "$MSG" 2>&1)" fi if [ $? -eq 0 ]; then echo "$TS OK telegram: $MSG" >> "$LOG" exit 0 fi # Zweitweg: direkt an die Bot-API. Das Token geht über --config auf stdin an curl, damit es nicht # in der Prozessliste steht. env_wert() { local wert wert="$(grep -E "^(export +)?$1=" "$TG_ENV" 2>/dev/null | tail -n 1)" wert="${wert#*=}" wert="${wert%$'\r'}" wert="${wert%\"}"; wert="${wert#\"}" wert="${wert%\'}"; wert="${wert#\'}" printf '%s' "$wert" } # Warum der Zweitweg scheiterte, steht seit 24.09.2026 mit im FALLBACK-Eintrag (der Telegram-Test der Oberfläche # zeigt es an). Ohne Klammern im Text: services/update_verlauf.py trennt den Eintrag an „): “. DIREKT_GRUND="" telegram_direkt() { local token chat thread text rc if [ ! -r "$TG_ENV" ]; then DIREKT_GRUND="Zugangsdatei $TG_ENV fehlt oder ist nicht lesbar" return 1 fi token="$(env_wert TELEGRAM_BOT_TOKEN)" chat="$(env_wert TELEGRAM_HOME_CHANNEL)" thread="$(env_wert TELEGRAM_HOME_CHANNEL_THREAD_ID)" if [ -z "$token" ] || [ -z "$chat" ]; then DIREKT_GRUND="TELEGRAM_BOT_TOKEN oder TELEGRAM_HOME_CHANNEL fehlt in $TG_ENV" return 1 fi text="$MSG" [ -n "$KOPF" ] && text="$KOPF $MSG" printf 'url = "%s/bot%s/sendMessage"\n' "$TG_API" "$token" \ | curl -sf -m 15 -o /dev/null --config - \ --data-urlencode "chat_id=$chat" --data-urlencode "text=$text" \ ${thread:+--data-urlencode "message_thread_id=$thread"} rc=$? case "$rc" in 0) return 0 ;; 22) DIREKT_GRUND="Telegram lehnt ab, HTTP-Fehler – Token oder Chat falsch?" ;; 6|7) DIREKT_GRUND="Telegram nicht erreichbar, keine Verbindung" ;; 28) DIREKT_GRUND="Telegram antwortet nicht rechtzeitig" ;; *) DIREKT_GRUND="curl endete mit Code $rc" ;; esac return 1 } if telegram_direkt; then # „OK telegram direkt“: services/update_verlauf.py erkennt beide Wege. echo "$TS OK telegram direkt: $MSG" >> "$LOG" exit 0 fi # Letzter Ausweg: Logfile + wall — Meldung darf nie verloren gehen. GRUND="$SEND_OUT" [ -n "$DIREKT_GRUND" ] && GRUND="${GRUND:+$GRUND; }Zweitweg: $DIREKT_GRUND" echo "$TS FALLBACK (telegram fehlgeschlagen: $GRUND): $MSG" >> "$LOG" if [ "${MC_NOTIFY_OHNE_WALL:-0}" != "1" ]; then printf '%s\n' "MC2-Meldung: ${SUBJECT:+$SUBJECT }$MSG" | wall 2>/dev/null || true fi echo "WARN: Telegram fehlgeschlagen, in $LOG protokolliert" >&2 [ "${MC_NOTIFY_STRICT:-0}" = "1" ] && exit 1 exit 0