from __future__ import annotations import json from dataclasses import dataclass from datetime import timedelta from urllib.parse import urljoin from flask import current_app from pywebpush import WebPushException, webpush from ..extensions import db from ..models import NotificationLog, PushSubscription, TaskInstance, User from .monthly import archive_months_missing_up_to_previous, get_snapshot_rows from .dates import local_now, previous_month @dataclass class NotificationResult: sent: int = 0 skipped: int = 0 failed: int = 0 def push_enabled() -> bool: return bool(current_app.config["VAPID_PUBLIC_KEY"] and current_app.config["VAPID_PRIVATE_KEY"]) def _absolute_url(path: str) -> str: base = current_app.config["APP_BASE_URL"].rstrip("/") + "/" return urljoin(base, path.lstrip("/")) def _notification_exists(user_id: int, notification_type: str, payload: dict) -> bool: payload_value = json.dumps(payload, sort_keys=True) return ( NotificationLog.query.filter_by(user_id=user_id, type=notification_type, payload=payload_value).first() is not None ) def _log_notification(user_id: int, notification_type: str, payload: dict) -> None: db.session.add( NotificationLog(user_id=user_id, type=notification_type, payload=json.dumps(payload, sort_keys=True)) ) def _send_subscription(subscription: PushSubscription, payload: dict) -> bool: try: webpush( subscription_info={ "endpoint": subscription.endpoint, "keys": {"p256dh": subscription.p256dh, "auth": subscription.auth}, }, data=json.dumps(payload), vapid_private_key=current_app.config["VAPID_PRIVATE_KEY"], vapid_claims={"sub": current_app.config["VAPID_CLAIMS_SUBJECT"]}, ) return True except WebPushException: return False def send_due_notifications() -> NotificationResult: result = NotificationResult() if not push_enabled(): result.skipped += 1 return result today = local_now().date() relevant_tasks = TaskInstance.query.filter( TaskInstance.completed_at.is_(None), TaskInstance.assigned_user_id.isnot(None), TaskInstance.due_date <= today + timedelta(days=1), ).all() for task in relevant_tasks: user = task.assigned_user if not user or not user.notification_task_due_enabled: result.skipped += 1 continue payload_marker = {"task_instance_id": task.id, "due_date": task.due_date.isoformat()} if _notification_exists(user.id, "task_due", payload_marker): result.skipped += 1 continue subscriptions = PushSubscription.query.filter_by(user_id=user.id).all() if not subscriptions: result.skipped += 1 continue body = "Heute ist ein guter Tag für Punkte." if task.due_date <= today else "Morgen wird's fällig." payload = { "title": f"Putzliga erinnert: {task.title}", "body": body, "icon": _absolute_url("/static/images/pwa-icon-192.png"), "badge": _absolute_url("/static/images/pwa-badge.png"), "url": _absolute_url("/my-tasks"), "tag": f"task-{task.id}", } sent_any = False for subscription in subscriptions: if _send_subscription(subscription, payload): sent_any = True result.sent += 1 else: result.failed += 1 if sent_any: _log_notification(user.id, "task_due", payload_marker) db.session.commit() return result def send_monthly_winner_notifications() -> NotificationResult: result = NotificationResult() if not push_enabled(): result.skipped += 1 return result now = local_now() if not (now.day == 1 and now.hour >= 9): result.skipped += 1 return result archive_months_missing_up_to_previous() target_year, target_month = previous_month(now.year, now.month) rows = get_snapshot_rows(target_year, target_month) if not rows: result.skipped += 1 return result winners = [row.user.name for row in rows if row.rank == 1] winner_text = ", ".join(winners) users = User.query.order_by(User.name.asc()).all() marker = {"year": target_year, "month": target_month} for user in users: if not user.notification_monthly_winner_enabled: result.skipped += 1 continue if _notification_exists(user.id, "monthly_winner", marker): result.skipped += 1 continue subscriptions = PushSubscription.query.filter_by(user_id=user.id).all() if not subscriptions: result.skipped += 1 continue payload = { "title": "Der Haushalts-Champion des letzten Monats steht fest", "body": f"{winner_text} führt den letzten Monat an. Schau ins Scoreboard.", "icon": _absolute_url("/static/images/pwa-icon-192.png"), "badge": _absolute_url("/static/images/pwa-badge.png"), "url": _absolute_url(f"/scoreboard?archive={target_year}-{target_month:02d}"), "tag": f"winner-{target_year}-{target_month}", } sent_any = False for subscription in subscriptions: if _send_subscription(subscription, payload): sent_any = True result.sent += 1 else: result.failed += 1 if sent_any: _log_notification(user.id, "monthly_winner", marker) db.session.commit() return result