""" User-level notification dispatcher. Channels: - 'ui' → no-op (dashboard shows the events anyway) - 'telegram' → per-user bot token + chat id """ import logging from typing import Optional import requests import db from settings import PUBLIC_URL logger = logging.getLogger("web.notifications") EventType = str # 'match' | 'apply_ok' | 'apply_fail' def _telegram_send(token: str, chat_id: str, text: str) -> bool: try: r = requests.post( f"https://api.telegram.org/bot{token}/sendMessage", json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown", "disable_web_page_preview": True}, timeout=10, ) if not r.ok: logger.warning("telegram send failed: %s %s", r.status_code, r.text[:200]) return False return True except requests.RequestException as e: logger.warning("telegram unreachable: %s", e) return False def _should_notify(notif, event: EventType) -> bool: if event == "match": return bool(notif["notify_on_match"]) if event == "apply_ok": return bool(notif["notify_on_apply_success"]) if event == "apply_fail": return bool(notif["notify_on_apply_fail"]) return False def notify_user(user_id: int, event: EventType, *, subject: str, body_plain: str, body_markdown: Optional[str] = None) -> None: """Fire a notification for one user on one event. Best-effort, non-raising.""" try: notif = db.get_notifications(user_id) if not notif or not _should_notify(notif, event): return channel = notif["channel"] or "ui" if channel == "telegram": token = notif["telegram_bot_token"] chat = notif["telegram_chat_id"] if token and chat: _telegram_send(token, chat, body_markdown or body_plain) except Exception: logger.exception("notify_user failed for user=%s event=%s", user_id, event) # -- Convenience builders ----------------------------------------------------- def on_match(user_id: int, flat: dict) -> None: addr = flat.get("address") or flat.get("link") rent = flat.get("total_rent") rooms = flat.get("rooms") link = flat.get("link", "") body = f"Neue passende Wohnung: {addr}\nMiete: {rent} €\nZimmer: {rooms}\n{link}" md = (f"*Neue passende Wohnung*\n[{addr}]({link})\n" f"Miete: {rent} € · Zimmer: {rooms}\n{PUBLIC_URL}") notify_user(user_id, "match", subject="[lazyflat] passende Wohnung", body_plain=body, body_markdown=md) def on_apply_ok(user_id: int, flat: dict, message: str) -> None: addr = flat.get("address") or flat.get("link") body = f"Bewerbung erfolgreich: {addr}\n{message}" md = f"*Bewerbung erfolgreich*\n{addr}\n{message}" notify_user(user_id, "apply_ok", subject="[lazyflat] Bewerbung OK", body_plain=body, body_markdown=md) def on_apply_fail(user_id: int, flat: dict, message: str) -> None: addr = flat.get("address") or flat.get("link") body = f"Bewerbung fehlgeschlagen: {addr}\n{message}\n{PUBLIC_URL}/bewerbungen" md = (f"*Bewerbung fehlgeschlagen*\n{addr}\n{message}\n" f"[Bewerbungen ansehen]({PUBLIC_URL}/bewerbungen)") notify_user(user_id, "apply_fail", subject="[lazyflat] Bewerbung fehlgeschlagen", body_plain=body, body_markdown=md)