Default push backend to ntfy; FCM opt-in with privacy warning (issue #10)
- IRIS_PUSH_BACKEND now defaults to ntfy (keeps push metadata on your own infrastructure); FCM is opt-in via IRIS_PUSH_BACKEND=fcm - build_push_backend(): ntfy for empty/unknown names, FCM only on explicit 'fcm' - gateway setup: warn when FCM is chosen (metadata routed via Google's servers) - README: privacy note + dedicated push section; new docs/playstore-listing.md with the FCM/ntfy privacy note for the Play Store listing - docs: 00/02/03/08/12/16 + setup.md updated to ntfy-default wording - tests: default-backend assertion updated (86/86 pass)
This commit is contained in:
1 parent
70282dfb65
commit
746d809d48
14 files changed
+136
-56
No files matched your search
@@ -33,8 +33,8 @@ register the (delivery-validated) file in the media registry and emit
|
||||
``validate_media_delivery_path`` at pull time.
|
||||
|
||||
Milestone M5: push + offline. Frames with no live subscriber are parked in
|
||||
the outbox (M3) AND wake the device via the push backend (``push.py``: FCM
|
||||
HTTP v1 primary, ntfy fallback, selected by ``IRIS_PUSH_BACKEND``).
|
||||
the outbox (M3) AND wake the device via the push backend (``push.py``: ntfy
|
||||
default, FCM HTTP v1 as an option, selected by ``IRIS_PUSH_BACKEND``).
|
||||
``notification`` frames render in-app banners and mirror to push (channel
|
||||
events, cron deliveries, approvals, clarifies); high-priority kinds push even
|
||||
when a device is live. ``fcm.register`` rotates push tokens (registry + live
|
||||
@@ -526,7 +526,7 @@ DEFAULT_PORT = 8790
|
||||
DEFAULT_HTTP_PORT = 8791 # docs/19: HTTP fallback leg
|
||||
DEFAULT_HOME_CHANNEL = "default"
|
||||
DEFAULT_HOME_CHANNEL_NAME = "Default"
|
||||
DEFAULT_PUSH_BACKEND = "fcm"
|
||||
DEFAULT_PUSH_BACKEND = "ntfy"
|
||||
DEFAULT_OUTBOX_RETENTION_HOURS = 72
|
||||
DEFAULT_MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # 100 MB
|
||||
|
||||
@@ -1201,10 +1201,17 @@ def interactive_setup() -> None:
|
||||
)
|
||||
save_env_value("IRIS_HTTP_PORT", str(_parse_port(port)))
|
||||
backend = prompt(
|
||||
"Push backend (fcm/ntfy)",
|
||||
"Push backend (ntfy/fcm)",
|
||||
default=get_env_value("IRIS_PUSH_BACKEND") or DEFAULT_PUSH_BACKEND,
|
||||
)
|
||||
save_env_value("IRIS_PUSH_BACKEND", (backend or DEFAULT_PUSH_BACKEND).strip().lower())
|
||||
backend = (backend or DEFAULT_PUSH_BACKEND).strip().lower()
|
||||
save_env_value("IRIS_PUSH_BACKEND", backend)
|
||||
if backend == "fcm":
|
||||
print_warning(
|
||||
"FCM push metadata (notification title, device token) is routed "
|
||||
"through Google's servers. For truly private communication use "
|
||||
"ntfy (self-hosted) instead."
|
||||
)
|
||||
|
||||
# Pairing payload for the app's Connect screen (manual entry + QR scan).
|
||||
# Advertise a routable host: a bind wildcard (0.0.0.0/127.0.0.1) is
|
||||
@@ -1346,7 +1353,7 @@ class IrisAdapter(BasePlatformAdapter):
|
||||
# /fast): picker_id -> pending state. In-memory only — a gateway
|
||||
# restart expires them (a stale picker.select is a no-op).
|
||||
self._pending_pickers: dict[str, dict] = {}
|
||||
# M5: push backend (FCM primary, ntfy fallback) + the throttle for
|
||||
# M5: push backend (ntfy default, FCM optional) + the throttle for
|
||||
# the outbox-prune banner.
|
||||
self._push: PushBackend = build_push_backend(
|
||||
self.push_backend,
|
||||
|
||||
@@ -38,7 +38,7 @@ optional_env:
|
||||
prompt: "Allow all devices? (true/false)"
|
||||
password: false
|
||||
- name: IRIS_PUSH_BACKEND
|
||||
description: "Push backend: fcm (default) or ntfy"
|
||||
description: "Push backend: ntfy (default, keeps metadata off Google) or fcm"
|
||||
prompt: "Push backend"
|
||||
password: false
|
||||
- name: IRIS_FCM_SERVICE_ACCOUNT
|
||||
|
||||
+21
-14
@@ -1,4 +1,4 @@
|
||||
"""Push backends: FCM (primary) + ntfy (fallback).
|
||||
"""Push backends: ntfy (default) + FCM (optional).
|
||||
|
||||
``PushBackend`` interface with two implementations:
|
||||
- ``FcmBackend``: FCM HTTP v1 via ``httpx`` + a Firebase service account
|
||||
@@ -8,7 +8,7 @@
|
||||
(default ``https://ntfy.sh``) via ``httpx``; the app's listener
|
||||
subscribes to the topic.
|
||||
|
||||
Selected by ``IRIS_PUSH_BACKEND`` (``fcm`` default, ``ntfy`` fallback).
|
||||
Selected by ``IRIS_PUSH_BACKEND`` (``ntfy`` default, ``fcm`` optional).
|
||||
Fired when a frame has no live subscriber; the data payload drives a silent
|
||||
sync on the device (docs/08-push.md).
|
||||
|
||||
@@ -33,7 +33,9 @@ import httpx
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FCM_SCOPE = "https://www.googleapis.com/auth/firebase.messaging"
|
||||
FCM_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
# Not a secret: the well-known Google OAuth2 token endpoint.
|
||||
# pi-lens-ignore: S105
|
||||
FCM_TOKEN_URL = "https://oauth2.googleapis.com/token" # noqa: S105
|
||||
FCM_V1_SEND_URL = "https://fcm.googleapis.com/v1/projects/{project_id}/messages:send"
|
||||
FCM_LEGACY_SEND_URL = "https://fcm.googleapis.com/fcm/send"
|
||||
# Refresh the cached access token this long before its expiry.
|
||||
@@ -54,7 +56,7 @@ class PushBackend:
|
||||
name: str = "push"
|
||||
# DeviceRegistry column that carries this backend's target token.
|
||||
# Not a secret: a DB column name (string literal), not a credential.
|
||||
# pi-lens-ignore: python-hardcoded-secrets
|
||||
# pi-lens-ignore: S105, python-hardcoded-secrets
|
||||
token_field: str = ""
|
||||
|
||||
def configured(self) -> bool:
|
||||
@@ -87,8 +89,8 @@ class FcmBackend(PushBackend):
|
||||
|
||||
name = "fcm"
|
||||
# Not a secret: a DB column name (string literal), not a credential.
|
||||
# pi-lens-ignore: python-hardcoded-secrets
|
||||
token_field = "fcm_token"
|
||||
# pi-lens-ignore: S105
|
||||
token_field = "fcm_token" # noqa: S105
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -257,8 +259,8 @@ class NtfyBackend(PushBackend):
|
||||
|
||||
name = "ntfy"
|
||||
# Not a secret: a DB column name (string literal), not a credential.
|
||||
# pi-lens-ignore: python-hardcoded-secrets
|
||||
token_field = "ntfy_topic"
|
||||
# pi-lens-ignore: S105
|
||||
token_field = "ntfy_topic" # noqa: S105
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -332,9 +334,14 @@ def build_push_backend(
|
||||
ntfy_server_url: str | None = None,
|
||||
ntfy_auth_token: str | None = None,
|
||||
) -> PushBackend:
|
||||
"""Select the backend by name (``IRIS_PUSH_BACKEND``; fcm default)."""
|
||||
if (name or "").strip().lower() == "ntfy":
|
||||
return NtfyBackend(
|
||||
topic=ntfy_topic, server_url=ntfy_server_url, auth_token=ntfy_auth_token
|
||||
)
|
||||
return FcmBackend(service_account=fcm_service_account, server_key=fcm_server_key)
|
||||
"""Select the backend by name (``IRIS_PUSH_BACKEND``; ntfy default).
|
||||
|
||||
ntfy is the default: it keeps push metadata on your own infrastructure.
|
||||
FCM is opt-in (``IRIS_PUSH_BACKEND=fcm``) — its metadata (title, device
|
||||
token) is routed through Google's servers.
|
||||
"""
|
||||
if (name or "").strip().lower() == "fcm":
|
||||
return FcmBackend(service_account=fcm_service_account, server_key=fcm_server_key)
|
||||
return NtfyBackend(
|
||||
topic=ntfy_topic, server_url=ntfy_server_url, auth_token=ntfy_auth_token
|
||||
)
|
||||
@@ -1404,7 +1404,7 @@ def test_push_backend_selection(plugin):
|
||||
push = plugin.push
|
||||
assert isinstance(push.build_push_backend("fcm"), push.FcmBackend)
|
||||
assert isinstance(push.build_push_backend("ntfy"), push.NtfyBackend)
|
||||
assert isinstance(push.build_push_backend(None), push.FcmBackend) # default
|
||||
assert isinstance(push.build_push_backend(None), push.NtfyBackend) # default
|
||||
assert isinstance(push.build_push_backend(" NTFY "), push.NtfyBackend)
|
||||
|
||||
assert push.build_push_backend("ntfy", ntfy_topic="my-topic").configured() is True
|
||||
|
||||
Reference in new issue
Block a user