|
# retoor <retoor@molodetz.nl>
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from devplacepy import stealth
|
|
from devplacepy.database import get_int_setting
|
|
from devplacepy.push import providers, store
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TIMEOUT_KEY = "push_delivery_timeout_seconds"
|
|
DEFAULT_TIMEOUT_SECONDS = 10
|
|
MIN_TIMEOUT_SECONDS = 1
|
|
MAX_TIMEOUT_SECONDS = 120
|
|
|
|
|
|
def timeout_seconds() -> float:
|
|
seconds = get_int_setting(TIMEOUT_KEY, DEFAULT_TIMEOUT_SECONDS)
|
|
return float(min(max(seconds, MIN_TIMEOUT_SECONDS), MAX_TIMEOUT_SECONDS))
|
|
|
|
|
|
def group_by_provider(
|
|
registrations: list[dict[str, Any]],
|
|
) -> dict[str, list[dict[str, Any]]]:
|
|
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
for registration in registrations:
|
|
grouped.setdefault(store.provider_of(registration), []).append(registration)
|
|
return grouped
|
|
|
|
|
|
async def notify_user(user_uid: str, payload: dict[str, Any]) -> None:
|
|
registrations = store.active_for_user(user_uid)
|
|
if not registrations:
|
|
logger.debug("No active push subscriptions for user %s", user_uid)
|
|
return
|
|
|
|
grouped = group_by_provider(registrations)
|
|
async with stealth.stealth_async_client(timeout=timeout_seconds()) as client:
|
|
for name, rows in grouped.items():
|
|
provider = providers.PROVIDERS.get(name)
|
|
if provider is None:
|
|
logger.warning(
|
|
"Unknown push provider %s on %s subscriptions of user %s",
|
|
name,
|
|
len(rows),
|
|
user_uid,
|
|
)
|
|
continue
|
|
if not providers.is_active(provider):
|
|
logger.debug(
|
|
"Push provider %s is not active; skipping %s subscriptions",
|
|
name,
|
|
len(rows),
|
|
)
|
|
continue
|
|
try:
|
|
prepared = provider.prepare(payload)
|
|
except Exception as exc:
|
|
logger.error("Push provider %s could not build a payload: %s", name, exc)
|
|
continue
|
|
for registration in rows:
|
|
await _deliver_one(provider, client, registration, prepared, user_uid)
|
|
|
|
|
|
async def _deliver_one(provider, client, registration, prepared, user_uid) -> None:
|
|
try:
|
|
outcome = await provider.deliver(client, registration, prepared)
|
|
except Exception as exc:
|
|
logger.error("Push provider %s raised for %s: %s", provider.name, user_uid, exc)
|
|
return
|
|
if outcome.status == providers.ACCEPTED:
|
|
logger.debug("Push delivered to %s via %s", user_uid, provider.name)
|
|
return
|
|
if outcome.status == providers.DEAD:
|
|
try:
|
|
store.mark_dead(registration["id"])
|
|
except Exception as exc:
|
|
logger.error("Could not soft-delete push subscription: %s", exc)
|
|
return
|
|
logger.warning(
|
|
"Push rejected by %s for %s: %s", provider.name, user_uid, outcome.detail
|
|
)
|