2026-06-25 03:02:52 +02:00
|
|
|
"""Manuell ausgelöster Notruf — vom Nutzer bewusst bestätigt.
|
2026-06-17 05:29:30 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
KEINE automatische Erkennung (zu unzuverlässig/intransparent). Ein Notruf
|
|
|
|
|
entsteht nur durch eine ausdrückliche Nutzeraktion (Knopf + Bestätigung).
|
2026-06-18 03:24:25 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
Testphase: Bei einem Notruf wird eine E-Mail an die Kontaktperson(en) des
|
|
|
|
|
Nutzers geschickt (Default: settings.emergency_contact_email). SMS/Anruf-
|
|
|
|
|
Bridges folgen später. Ohne konfigurierten SMTP-Host wird nichts versendet
|
|
|
|
|
und der Nutzer erhält einen klaren Hinweis darauf.
|
2026-06-17 05:29:30 +02:00
|
|
|
"""
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
import asyncio
|
|
|
|
|
import logging
|
|
|
|
|
import smtplib
|
|
|
|
|
from email.message import EmailMessage
|
|
|
|
|
|
|
|
|
|
from app.config import settings
|
2026-06-17 05:29:30 +02:00
|
|
|
from app.metrics import metrics
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# Hinweis je Sprache, wenn (noch) KEINE Benachrichtigung rausging.
|
2026-06-25 03:02:52 +02:00
|
|
|
_NO_NOTIFY_NOTICE: dict[str, str] = {
|
|
|
|
|
"de": "ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.",
|
|
|
|
|
"en": "ATTENTION: No notification has been set up yet.",
|
|
|
|
|
"fr": "ATTENTION : aucune notification n'est encore configurée.",
|
|
|
|
|
"es": "ATENCIÓN: todavía no se ha configurado ninguna notificación.",
|
|
|
|
|
"it": "ATTENZIONE: non è ancora stata configurata alcuna notifica.",
|
|
|
|
|
"pt": "ATENÇÃO: ainda não há nenhuma notificação configurada.",
|
|
|
|
|
"pl": "UWAGA: Nie skonfigurowano jeszcze żadnego powiadomienia.",
|
|
|
|
|
"ar": "تنبيه: لم يتم إعداد أي إشعار بعد.",
|
|
|
|
|
"ru": "ВНИМАНИЕ: Уведомление пока не настроено.",
|
|
|
|
|
"zh": "注意:尚未设置任何通知。",
|
2026-06-17 05:29:30 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
# Hinweis je Sprache, wenn die E-Mail rausging.
|
|
|
|
|
_SENT_NOTICE: dict[str, str] = {
|
|
|
|
|
"de": "Hilfe wurde verständigt. Eine Kontaktperson wurde per E-Mail benachrichtigt.",
|
|
|
|
|
"en": "Help has been alerted. A contact person was notified by email.",
|
|
|
|
|
"fr": "Les secours ont été alertés. Un proche a été prévenu par e-mail.",
|
|
|
|
|
"es": "Se ha avisado a ayuda. Se notificó a un contacto por correo electrónico.",
|
|
|
|
|
"it": "L'aiuto è stato allertato. Una persona di contatto è stata avvisata via e-mail.",
|
|
|
|
|
"pt": "A ajuda foi avisada. Uma pessoa de contato foi notificada por e-mail.",
|
|
|
|
|
"pl": "Pomoc została powiadomiona. Osoba kontaktowa otrzymała e-mail.",
|
|
|
|
|
"ar": "تم تنبيه المساعدة. تم إخطار شخص الاتصال عبر البريد الإلكتروني.",
|
|
|
|
|
"ru": "Помощь оповещена. Контактное лицо уведомлено по электронной почте.",
|
|
|
|
|
"zh": "已通知求助。已通过电子邮件通知联系人。",
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 05:29:30 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
def notify_notice(language: str | None, sent: bool) -> str:
|
|
|
|
|
"""Hinweis in Nutzersprache (Fallback Deutsch), je nachdem ob eine Mail rausging."""
|
|
|
|
|
table = _SENT_NOTICE if sent else _NO_NOTIFY_NOTICE
|
|
|
|
|
return table.get((language or "de").lower(), table["de"])
|
2026-06-18 03:24:25 +02:00
|
|
|
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
def _contacts(user, cfg) -> list[str]:
|
|
|
|
|
"""Kontakt-E-Mail(s): pro-Nutzer-Pref (CSV) falls vorhanden, sonst Default aus der Config."""
|
|
|
|
|
prefs = getattr(user, "prefs", None) or {}
|
|
|
|
|
raw = (prefs.get("emergency_contacts") or "").strip() if isinstance(prefs, dict) else ""
|
|
|
|
|
if not raw:
|
|
|
|
|
raw = cfg.emergency_contact_email or ""
|
|
|
|
|
return [a.strip() for a in raw.replace(";", ",").split(",") if a.strip()]
|
2026-06-18 03:24:25 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
|
|
|
|
|
def _send_email_sync(to: list[str], subject: str, body: str, cfg) -> bool:
|
|
|
|
|
"""Versendet eine E-Mail per SMTP. Liefert True bei Erfolg, sonst False (best effort)."""
|
|
|
|
|
if not cfg.smtp_host or not to:
|
|
|
|
|
return False
|
|
|
|
|
msg = EmailMessage()
|
|
|
|
|
msg["From"] = cfg.smtp_from or cfg.smtp_user or "voice-assistant@localhost"
|
|
|
|
|
msg["To"] = ", ".join(to)
|
|
|
|
|
msg["Subject"] = subject
|
|
|
|
|
msg.set_content(body)
|
|
|
|
|
try:
|
|
|
|
|
with smtplib.SMTP(cfg.smtp_host, int(cfg.smtp_port), timeout=10) as s:
|
|
|
|
|
if cfg.smtp_starttls:
|
|
|
|
|
s.starttls()
|
|
|
|
|
if cfg.smtp_user:
|
|
|
|
|
s.login(cfg.smtp_user, cfg.smtp_password)
|
|
|
|
|
s.send_message(msg)
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("Notruf-E-Mail konnte nicht versendet werden")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def record_manual_emergency(user, store, language: str | None = None, cfg=settings) -> dict:
|
|
|
|
|
"""Protokolliert einen bestätigten Notruf, benachrichtigt Kontakte und liefert den Hinweis."""
|
2026-06-25 03:02:52 +02:00
|
|
|
store.log_emergency(user.id, "manual", "Vom Nutzer ausgelöster Notruf")
|
|
|
|
|
metrics.inc("emergency_total", {"category": "manual", "source": "user"})
|
2026-06-25 03:37:18 +02:00
|
|
|
|
|
|
|
|
to = _contacts(user, cfg)
|
|
|
|
|
name = getattr(user, "display_name", None) or getattr(user, "id", "?")
|
|
|
|
|
subject = f"🆘 Notruf von {name}"
|
|
|
|
|
body = (
|
|
|
|
|
f"{name} hat in der Voice-Assistant-App einen Notruf ausgelöst.\n\n"
|
|
|
|
|
f"Bitte umgehend Kontakt aufnehmen.\n\n"
|
|
|
|
|
f"(Testphase: automatische Eskalation per SMS/Anruf ist noch nicht aktiv.)"
|
|
|
|
|
)
|
|
|
|
|
sent = await asyncio.to_thread(_send_email_sync, to, subject, body, cfg)
|
|
|
|
|
if sent:
|
|
|
|
|
metrics.inc("emergency_notify_total", {"channel": "email"})
|
|
|
|
|
return {"category": "manual", "notice": notify_notice(language, sent), "email_sent": sent}
|