Files
chore/backend/utils/push_sender.py
Ryan Kegel 6bf10fda2f
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m23s
feat: implement routines feature for child and parent modes
- Added backend models for routines, routine items, and schedules.
- Created API endpoints for managing routines and their items.
- Implemented frontend components for routine creation, detail view, and assignment.
- Integrated routines into child view with a scrolling list and approval workflow.
- Added push notifications for routine confirmations and pending approvals.
- Refactored existing components to accommodate new routines functionality.
- Updated tests to cover new routines feature and ensure proper functionality.
2026-04-28 23:30:52 -04:00

76 lines
2.6 KiB
Python

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