from __future__ import annotations import json from typing import Any import aiosqlite from pywebpush import WebPushException, webpush from server.db import utc_now_iso from server.settings import settings async def save_subscription(db: aiosqlite.Connection, sub: dict[str, Any]) -> None: keys = sub.get("keys", {}) await db.execute( """ INSERT INTO push_subscription (endpoint, p256dh, auth, created_at) VALUES (?, ?, ?, ?) ON CONFLICT(endpoint) DO UPDATE SET p256dh=excluded.p256dh, auth=excluded.auth """, (sub["endpoint"], keys.get("p256dh", ""), keys.get("auth", ""), utc_now_iso()), ) await db.commit() async def get_subscriptions(db: aiosqlite.Connection) -> list[dict[str, str]]: cur = await db.execute("SELECT endpoint, p256dh, auth FROM push_subscription") rows = await cur.fetchall() await cur.close() return [{"endpoint": r[0], "p256dh": r[1], "auth": r[2]} for r in rows] def _vapid_claims() -> dict[str, str]: return {"sub": settings.vapid_claims_email} async def send_push( db: aiosqlite.Connection, payload: dict[str, Any], ) -> int: if not settings.vapid_private_key or not settings.vapid_public_key: return 0 subs = await get_subscriptions(db) sent = 0 dead: list[str] = [] for sub in subs: subscription = { "endpoint": sub["endpoint"], "keys": {"p256dh": sub["p256dh"], "auth": sub["auth"]}, } try: webpush( subscription_info=subscription, data=json.dumps(payload), vapid_private_key=settings.vapid_private_key, vapid_claims=_vapid_claims(), ) sent += 1 except WebPushException as exc: if exc.response and exc.response.status_code in (404, 410): dead.append(sub["endpoint"]) for endpoint in dead: await db.execute("DELETE FROM push_subscription WHERE endpoint = ?", (endpoint,)) if dead: await db.commit() return sent async def send_slot_reminder( db: aiosqlite.Connection, slot_id: str, title: str, body: str, *, silent: bool = True, ) -> int: payload = { "title": title, "body": body, "slot_id": slot_id, "silent": silent, "tag": f"med-{slot_id}", } return await send_push(db, payload)