""" User-level notification dispatcher. Channels: - 'ui' → no-op (dashboard shows the events anyway) - 'telegram' → per-user bot token + chat id - 'email' → system SMTP (one outbox, per-user recipient) """ import logging import smtplib from email.mime.text import MIMEText from typing import Optional import requests import db from settings import ( PUBLIC_URL, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_PORT, SMTP_STARTTLS, SMTP_USERNAME, ) 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 _email_send(recipient: str, subject: str, body: str) -> bool: if not SMTP_HOST or not recipient: logger.info("email skipped (SMTP_HOST=%r recipient=%r)", SMTP_HOST, recipient) return False try: msg = MIMEText(body, _charset="utf-8") msg["Subject"] = subject msg["From"] = SMTP_FROM msg["To"] = recipient with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=15) as s: if SMTP_STARTTLS: s.starttls() if SMTP_USERNAME: s.login(SMTP_USERNAME, SMTP_PASSWORD) s.send_message(msg) logger.info("email sent to %s", recipient) return True except Exception as e: logger.warning("email send failed: %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 == "ui": return 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) elif channel == "email": addr = notif["email_address"] if addr: _email_send(addr, subject, 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}/fehler" md = (f"*Bewerbung fehlgeschlagen*\n{addr}\n{message}\n" f"[Fehler ansehen]({PUBLIC_URL}/fehler)") notify_user(user_id, "apply_fail", subject="[lazyflat] Bewerbung fehlgeschlagen", body_plain=body, body_markdown=md)