77 lines
1.8 KiB
Python
Raw Normal View History

Add push provider architecture with Apple Push Notification support Split the push delivery library into a provider architecture. devplacepy/push becomes a package: a PushProvider protocol with a registry, the existing Web Push implementation moved unchanged behind it, a new APNs provider, a store owning every push_registration access, and a delivery loop that groups a user's subscriptions by provider, prepares each provider's payload once and sends over a single shared client. APNs delivers over HTTP/2 with an ES256 provider token cached per credential fingerprint, so a worker signs at most one token per 45 minutes. Registrations carry a hexadecimal device token; 410 and the Unregistered class of reasons soft delete the subscription exactly like a gone Web Push endpoint. All provider configuration is edited at /admin/services/push through the same ConfigField surface every other subsystem uses, assembled from the registry so a future provider needs no edit to the service. A provider that is disabled, unconfigured or holding an unusable credential accepts no registrations and is skipped during delivery, never failing the other providers. POST /push.json accepts a registration for any active provider; a body without a provider field is a Web Push body, so existing clients are unchanged. GET /push.json keeps publicKey at the top level and adds the active providers. push_registration gains provider and token columns, ensured in init_db with a converging backfill; existing rows are never rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:43:10 +02:00
# retoor <retoor@molodetz.nl>
import logging
from typing import Any
from devplacepy.push.providers.apns import ApnsProvider
from devplacepy.push.providers.base import (
ACCEPTED,
DEAD,
REJECTED,
Delivery,
PushProvider,
)
from devplacepy.push.providers.webpush import WebPushProvider
logger = logging.getLogger(__name__)
DEFAULT_PROVIDER = WebPushProvider.name
PROVIDERS: dict[str, PushProvider] = {
provider.name: provider for provider in (WebPushProvider(), ApnsProvider())
}
__all__ = [
"ACCEPTED",
"DEAD",
"DEFAULT_PROVIDER",
"Delivery",
"PROVIDERS",
"PushProvider",
"REJECTED",
"active",
"admin_fields",
"client_config",
"get",
"is_active",
"names",
]
def get(name: str | None) -> PushProvider | None:
if not isinstance(name, str):
name = ""
return PROVIDERS.get(name.strip().lower() or DEFAULT_PROVIDER)
def names() -> list[str]:
return list(PROVIDERS)
def active() -> list[PushProvider]:
return [provider for provider in PROVIDERS.values() if is_active(provider)]
def admin_fields() -> list:
return [field for provider in PROVIDERS.values() for field in provider.all_fields()]
def client_config() -> dict[str, Any]:
return {provider.name: _client_config(provider) for provider in active()}
def is_active(provider: PushProvider) -> bool:
try:
return provider.is_active()
except Exception as exc:
logger.error("Push provider %s failed its readiness check: %s", provider.name, exc)
return False
def _client_config(provider: PushProvider) -> dict[str, Any]:
try:
return provider.client_config()
except Exception as exc:
logger.error("Push provider %s failed to describe itself: %s", provider.name, exc)
return {}