lazyflat/web/notifications.py
EiSiMo 64439fd42e feat(notifications): add Telegram test button
New "Test senden" button next to Speichern posts current form
credentials (not DB) to /actions/notifications/test, which fires a
test message and redirects back with a flash chip showing the outcome
(including the Telegram API's error description on failure).

telegram_send is now public and returns (ok, detail) so the UI can
surface real error messages ("chat not found", "Unauthorized", etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:35:02 +02:00

97 lines
3.7 KiB
Python

"""
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) -> tuple[bool, str]:
"""Send a Telegram message. Returns (ok, detail) — detail is the Telegram
API's `description` on failure, or a short error string, or "ok"."""
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 r.ok:
return True, "ok"
try:
detail = r.json().get("description") or f"HTTP {r.status_code}"
except ValueError:
detail = f"HTTP {r.status_code}"
logger.warning("telegram send failed: %s %s", r.status_code, r.text[:200])
return False, detail
except requests.RequestException as e:
logger.warning("telegram unreachable: %s", e)
return False, str(e)
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)