112 lines
3.5 KiB
Python
112 lines
3.5 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.membre import Membre
|
|
from app.models.push_subscription import PushSubscription
|
|
|
|
|
|
NOTIFICATION_TYPES = [
|
|
{"code": "gouvernance", "label": "Gouvernance"},
|
|
{"code": "tresorerie", "label": "Trésorerie"},
|
|
{"code": "membres", "label": "Membres"},
|
|
{"code": "postes", "label": "Postes et mandats"},
|
|
{"code": "rencontres", "label": "Rencontres et présences"},
|
|
{"code": "litterature", "label": "Littérature"},
|
|
{"code": "evenements", "label": "Événements"},
|
|
{"code": "systeme", "label": "Système"},
|
|
]
|
|
|
|
DEFAULT_NOTIFICATION_PREFERENCES = {item["code"]: True for item in NOTIFICATION_TYPES}
|
|
|
|
|
|
def normalized_preferences(raw: dict | None) -> dict:
|
|
prefs = DEFAULT_NOTIFICATION_PREFERENCES.copy()
|
|
if isinstance(raw, dict):
|
|
for code in prefs:
|
|
if code in raw:
|
|
prefs[code] = bool(raw[code])
|
|
return prefs
|
|
|
|
|
|
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 = "/",
|
|
category: str = "systeme",
|
|
) -> int:
|
|
if not notifications_configured() or not groupe_id:
|
|
return 0
|
|
subscriptions_result = await db.execute(
|
|
select(PushSubscription).where(
|
|
PushSubscription.groupe_id == groupe_id,
|
|
PushSubscription.membre_id != actor_id,
|
|
)
|
|
)
|
|
subscriptions = subscriptions_result.scalars().all()
|
|
member_ids = {sub.membre_id for sub in subscriptions}
|
|
if not member_ids:
|
|
return 0
|
|
membres_result = await db.execute(select(Membre).where(Membre.id.in_(member_ids)))
|
|
membres = {m.id: m for m in membres_result.scalars().all()}
|
|
filtered = [
|
|
sub for sub in subscriptions
|
|
if normalized_preferences(getattr(membres.get(sub.membre_id), "notification_preferences", None)).get(category, True)
|
|
]
|
|
return await send_to_subscriptions(db, filtered, {
|
|
"title": title,
|
|
"body": body,
|
|
"url": url,
|
|
"category": category,
|
|
})
|