groupe-meditation/backend/app/services/notifications.py
2026-05-25 17:39:38 -04:00

76 lines
2.1 KiB
Python

"""Envoi des notifications Web Push."""
import asyncio
import base64
import json
from tempfile import NamedTemporaryFile
from typing import Iterable
from pywebpush import WebPushException, webpush
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.models.push_subscription import PushSubscription
def notifications_configured() -> bool:
return bool(settings.VAPID_PUBLIC_KEY and settings.VAPID_PRIVATE_KEY_B64)
def _private_key_pem() -> str:
if not settings.VAPID_PRIVATE_KEY_B64:
return ""
return base64.b64decode(settings.VAPID_PRIVATE_KEY_B64).decode()
def _send_sync(abonnement: dict, payload: dict) -> None:
private_key = _private_key_pem()
with NamedTemporaryFile("w", suffix=".pem") as key_file:
key_file.write(private_key)
key_file.flush()
webpush(
subscription_info=abonnement,
data=json.dumps(payload, ensure_ascii=False),
vapid_private_key=key_file.name,
vapid_claims={"sub": settings.VAPID_SUBJECT},
)
async def send_to_subscriptions(db: AsyncSession, subscriptions: Iterable[PushSubscription], payload: dict) -> int:
if not notifications_configured():
return 0
sent = 0
for sub in subscriptions:
try:
await asyncio.to_thread(_send_sync, sub.abonnement, payload)
sent += 1
except WebPushException as exc:
if getattr(exc.response, "status_code", None) in {404, 410}:
await db.delete(sub)
except Exception:
continue
return sent
async def notify_group_change(
db: AsyncSession,
*,
groupe_id,
actor_id,
title: str,
body: str,
url: str = "/",
) -> int:
if not notifications_configured() or not groupe_id:
return 0
result = await db.execute(
select(PushSubscription).where(
PushSubscription.groupe_id == groupe_id,
PushSubscription.membre_id != actor_id,
)
)
return await send_to_subscriptions(db, result.scalars().all(), {
"title": title,
"body": body,
"url": url,
})