Spaces:
Running on Zero
Running on Zero
File size: 3,424 Bytes
4cd8837 4aaae80 4cd8837 4aaae80 4cd8837 4aaae80 4cd8837 4aaae80 4cd8837 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | """Push token registry for relay-mediated mobile notifications (M15)."""
from __future__ import annotations
import logging
import time
from dataclasses import dataclass
logger = logging.getLogger(__name__)
try:
import httpx
HAS_HTTPX = True
except ImportError:
httpx = None # type: ignore[assignment]
HAS_HTTPX = False
@dataclass
class PushTokenRecord:
"""Record of a mobile push token registered with a relay."""
node_id: str
platform: str # "apns" | "fcm" | "generic"
device_token: str
relay_url: str
registered_at: float
class PushSubscriber:
"""
Registers and unregisters APNs / FCM device tokens with a relay server.
Degrades gracefully when the relay is unreachable.
"""
def __init__(
self,
relay_url: str,
http_client: object | None = None,
) -> None:
self._relay_url = relay_url.rstrip("/")
self._http_client = http_client
self._httpx_client: object | None = None
# ββ Internal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _get_httpx(self) -> object:
if not HAS_HTTPX:
raise ImportError("httpx is required for PushSubscriber: pip install httpx")
if self._httpx_client is None:
import httpx as _httpx
self._httpx_client = _httpx.AsyncClient(timeout=10.0)
return self._httpx_client
async def _post(self, path: str, body: dict) -> dict | None:
url = f"{self._relay_url}{path}"
try:
client = self._get_httpx()
resp = await client.post(url, json=body) # type: ignore[union-attr]
resp.raise_for_status()
return resp.json()
except Exception as exc:
logger.warning("PushSubscriber POST %s failed: %s", path, exc)
return None
# ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async def register_push_token(
self,
node_id: str,
platform: str,
device_token: str,
) -> bool:
"""
Register a mobile push token with the relay.
Returns True on success, False on failure.
"""
payload = {
"node_id": node_id,
"platform": platform,
"device_token": device_token,
"registered_at": time.time(),
}
data = await self._post("/relay/v1/push/register", payload)
return bool(data and data.get("ok"))
async def unregister(self, node_id: str, device_token: str) -> bool:
"""
Remove a push token registration from the relay.
Returns True on success, False on failure.
"""
payload = {"node_id": node_id, "device_token": device_token}
data = await self._post("/relay/v1/push/unregister", payload)
return bool(data and data.get("ok"))
async def close(self) -> None:
"""Close the internal httpx client."""
if self._httpx_client is not None:
try:
await self._httpx_client.aclose() # type: ignore[union-attr]
except Exception:
pass
self._httpx_client = None
|