M5: push (FCM + ntfy) + offline catch-up + background notifications
Server (gateway-plugin): - push.py: FCM (HTTP v1 service-account / legacy key) + ntfy backends; NtfyBackend.server_url for app discovery - adapter.py: push on offline broadcast + high-priority push when live; fcm.register; server_caps.push_ntfy_server; outbox now ALWAYS appends so a reconnecting app catches up on live-delivered frames (fixes empty chat after notification tap / activity recreation) - protocol.py / ws_server.py / outbox.py / plugin.yaml: M5 frames + env vars App (Kotlin CMP): - Protocol.kt: notification / fcm.register / sync frames + push caps - GatewayClient.kt: hello carries push creds; auto-sync on reconnect - IrisController.kt: banners, deep-link, notifyMessageIfBackgrounded (system notification on a regular reply when backgrounded) - Android: PlatformPush, AppBridge, IrisNotifications, NtfyListenerService, IrisFirebaseMessagingService, AndroidPush, AndroidSecureStore - Desktop: DesktopPush, DesktopSecureStore - build files + manifest (permissions, services, deep-links) Tests: 35-test tests/gateway/test_android.py suite passes (incl. new regression test_live_delivered_frame_still_parked_for_sync). E2E verified on device: push fire, reconnect sync, background notification, and message replay after ChatStore reset.
This commit is contained in:
1 parent
913ee91024
commit
2ecfe1c05c
28 files changed
+1739
-51
No files matched your search
@@ -76,6 +76,10 @@ TYPE_MEDIA_OFFER = "media.offer"
|
||||
TYPE_MEDIA_PULL = "media.pull"
|
||||
TYPE_MEDIA_PULL_END = "media.pull.end"
|
||||
|
||||
# Push / notifications (M5)
|
||||
TYPE_NOTIFICATION = "notification"
|
||||
TYPE_FCM_REGISTER = "fcm.register"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error codes (``error`` frame payload.code)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -96,6 +100,22 @@ ROLE_ASSISTANT = "assistant"
|
||||
ROLE_SYSTEM = "system"
|
||||
ROLE_CRON = "cron"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification kinds (``notification`` frame payload.kind, M5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NOTIF_CHANNEL_CREATED = "channel_created"
|
||||
NOTIF_CHANNEL_RENAMED = "channel_renamed"
|
||||
NOTIF_CHANNEL_DELETED = "channel_deleted"
|
||||
NOTIF_CRON = "cron"
|
||||
NOTIF_APPROVAL = "approval"
|
||||
NOTIF_CLARIFY = "clarify"
|
||||
NOTIF_GENERIC = "generic"
|
||||
|
||||
# Kinds that push even when a device is live (the app may be backgrounded;
|
||||
# it decides whether to also show an in-app banner).
|
||||
HIGH_PRIORITY_NOTIF_KINDS = frozenset({NOTIF_APPROVAL, NOTIF_CLARIFY, NOTIF_CRON})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Envelope
|
||||
@@ -452,6 +472,41 @@ def sync_done(cursor: int, *, id: Optional[int] = None) -> Frame:
|
||||
return Frame(type=TYPE_SYNC_DONE, id=id, payload={"cursor": cursor})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Push / notification frames (M5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def notification(
|
||||
chat_id: str,
|
||||
kind: str,
|
||||
title: str,
|
||||
body: str,
|
||||
*,
|
||||
thread_id: Optional[str] = None,
|
||||
ts: Optional[int] = None,
|
||||
) -> Frame:
|
||||
"""Event: a transient in-app banner (and a push mirror when the device is
|
||||
offline). ``kind`` is one of the ``NOTIF_*`` constants."""
|
||||
payload: Dict[str, Any] = {"kind": kind, "title": title, "body": body}
|
||||
if ts is not None:
|
||||
payload["ts"] = ts
|
||||
return Frame(
|
||||
type=TYPE_NOTIFICATION, chat_id=chat_id, thread_id=thread_id, payload=payload
|
||||
)
|
||||
|
||||
|
||||
def fcm_register(
|
||||
fcm_token: Optional[str] = None, ntfy_topic: Optional[str] = None
|
||||
) -> Frame:
|
||||
"""Request: update the device's push tokens (FCM rotation / ntfy topic)."""
|
||||
payload: Dict[str, Any] = {}
|
||||
if fcm_token:
|
||||
payload["fcm_token"] = fcm_token
|
||||
if ntfy_topic:
|
||||
payload["ntfy_topic"] = ntfy_topic
|
||||
return Frame(type=TYPE_FCM_REGISTER, payload=payload)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Media frames (M4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user