import json import logging import os from pywebpush import webpush, WebPushException from db.push_subscriptions import get_subscriptions_by_user, delete_subscription_by_id logger = logging.getLogger(__name__) def _get_vapid_private_key() -> str | None: return os.environ.get('VAPID_PRIVATE_KEY') def _get_vapid_claims_email() -> str: return os.environ.get('VAPID_CLAIMS_EMAIL', 'admin@reward-app.local') def send_push_to_user(user_id: str, payload: dict) -> int: """ Send a web push notification to all stored subscriptions for the user. Returns the number of subscriptions successfully notified. Stale/expired subscriptions (HTTP 404/410) are removed automatically. """ private_key = _get_vapid_private_key() if not private_key: logger.warning('VAPID_PRIVATE_KEY not configured; skipping push notification') return 0 subscriptions = get_subscriptions_by_user(user_id) if not subscriptions: return 0 claims_email = _get_vapid_claims_email() sent = 0 for sub in subscriptions: try: subscription_info = { 'endpoint': sub.endpoint, 'keys': sub.keys, } webpush( subscription_info=subscription_info, data=json.dumps(payload), vapid_private_key=private_key, vapid_claims={'sub': f'mailto:{claims_email}'}, ttl=86400, ) sent += 1 except WebPushException as e: status_code = e.response.status_code if e.response is not None else None if status_code in (404, 410): # Subscription is gone — clean it up logger.info( f'Removing stale push subscription {sub.id} for user {user_id} (HTTP {status_code})' ) delete_subscription_by_id(sub.id) elif status_code == 403 and 'VAPID credentials' in str(e): # The subscription was created with a different VAPID keypair. # Remove it so the client can register a fresh subscription next time. logger.info( f'Removing VAPID-mismatched subscription {sub.id} for user {user_id} (HTTP 403)' ) delete_subscription_by_id(sub.id) else: logger.error( f'WebPushException for subscription {sub.id} (user {user_id}): {e}' ) except Exception as e: logger.error(f'Unexpected error sending push to subscription {sub.id}: {e}') return sent