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
@@ -8,6 +8,8 @@ Iris pairs with your running `hermes gateway` over a private WebSocket and gives
|
|||||||
|
|
||||||
- **Native Hermes-Gateway integration** — your hermes → gateway → Iris app
|
- **Native Hermes-Gateway integration** — your hermes → gateway → Iris app
|
||||||
- **Absolute Privacy!** — everything stays on your own infrastructure
|
- **Absolute Privacy!** — everything stays on your own infrastructure
|
||||||
|
(push: ntfy by default; FCM is opt-in and routes push metadata via Google —
|
||||||
|
see [Push notifications](#push-notifications))
|
||||||
- **No file limit**
|
- **No file limit**
|
||||||
- **No character limit**
|
- **No character limit**
|
||||||
- **Full markdown support** — tables, checkmarks, bold, inline code, code blocks + syntax highlighting…
|
- **Full markdown support** — tables, checkmarks, bold, inline code, code blocks + syntax highlighting…
|
||||||
@@ -36,7 +38,22 @@ hermes-agent ──> hermes gateway ──(WebSocket :8790)──> Iris app (And
|
|||||||
- The app is a first-class hermes *messaging platform*, so everything the gateway
|
- The app is a first-class hermes *messaging platform*, so everything the gateway
|
||||||
already does just works: slash commands, cron delivery, `send_message` routing,
|
already does just works: slash commands, cron delivery, `send_message` routing,
|
||||||
coexistence with Telegram/Discord/etc.
|
coexistence with Telegram/Discord/etc.
|
||||||
- Push notifications: FCM (primary) or ntfy (fallback).
|
- Push notifications: ntfy (default) or FCM (opt-in).
|
||||||
|
|
||||||
|
## Push notifications
|
||||||
|
|
||||||
|
Push wakes a backgrounded/offline device; on reconnect the app syncs the
|
||||||
|
outbox, so nothing is lost.
|
||||||
|
|
||||||
|
- **ntfy (default)** — push metadata stays on your own infrastructure
|
||||||
|
(self-hosted ntfy recommended). This is the backend for truly private
|
||||||
|
communication.
|
||||||
|
- **FCM (opt-in, `IRIS_PUSH_BACKEND=fcm`)** — standard/reliable, but FCM push
|
||||||
|
metadata (notification title, device token) is routed through **Google's
|
||||||
|
servers**. If you want truly private communication, use ntfy instead.
|
||||||
|
|
||||||
|
Setup: [`docs/setup.md`](docs/setup.md) §4; details:
|
||||||
|
[`docs/08-push.md`](docs/08-push.md).
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
@@ -99,7 +116,7 @@ Notes:
|
|||||||
- Remote access: Tailscale/WireGuard, or a reverse proxy with CA-signed WSS
|
- Remote access: Tailscale/WireGuard, or a reverse proxy with CA-signed WSS
|
||||||
(`ANDROID_WS_CERT` / `ANDROID_WS_KEY`).
|
(`ANDROID_WS_CERT` / `ANDROID_WS_KEY`).
|
||||||
|
|
||||||
Full walkthrough, push setup (FCM/ntfy), and troubleshooting:
|
Full walkthrough, push setup (ntfy/FCM), and troubleshooting:
|
||||||
[`docs/setup.md`](docs/setup.md).
|
[`docs/setup.md`](docs/setup.md).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ Everything in the feature checklist below.
|
|||||||
| Decision | Choice |
|
| Decision | Choice |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Desktop app tech | **Compose Multiplatform** (shares Android code; "tweaked" for big screen) |
|
| Desktop app tech | **Compose Multiplatform** (shares Android code; "tweaked" for big screen) |
|
||||||
| Push backend | **Both** — FCM primary, ntfy fallback (`IRIS_PUSH_BACKEND`) |
|
| Push backend | **Both** — ntfy default, FCM optional (`IRIS_PUSH_BACKEND`) |
|
||||||
| Media transport | **Over the WebSocket** (chunked binary frames; no extra Python deps) |
|
| Media transport | **Over the WebSocket** (chunked binary frames; no extra Python deps) |
|
||||||
| Phone default layout | **User-toggleable, single-pane default** (auto two-pane on large screens) |
|
| Phone default layout | **User-toggleable, single-pane default** (auto two-pane on large screens) |
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -61,8 +61,8 @@ iris_x_hermes/
|
|||||||
- **`media.py`** — inbound chunked upload → `cache_*_from_bytes`; outbound
|
- **`media.py`** — inbound chunked upload → `cache_*_from_bytes`; outbound
|
||||||
`media.offer`/`media.pull` chunked streaming.
|
`media.offer`/`media.pull` chunked streaming.
|
||||||
- **`outbox.py`** — SQLite outbox per `chat_id` + monotonic sync cursor.
|
- **`outbox.py`** — SQLite outbox per `chat_id` + monotonic sync cursor.
|
||||||
- **`push.py`** — `PushBackend` interface; `FcmBackend` (httpx, FCM HTTP v1) and
|
- **`push.py`** — `PushBackend` interface; `NtfyBackend` (default) and
|
||||||
`NtfyBackend` (reuses hermes ntfy publish). Selected by `IRIS_PUSH_BACKEND`.
|
`FcmBackend` (httpx, FCM HTTP v1). Selected by `IRIS_PUSH_BACKEND`.
|
||||||
- **`pairing.py`** — token generation/verification (constant-time), device
|
- **`pairing.py`** — token generation/verification (constant-time), device
|
||||||
registry (SQLite), QR payload.
|
registry (SQLite), QR payload.
|
||||||
- **`search.py`** — FTS5 query bridge over the hermes session store.
|
- **`search.py`** — FTS5 query bridge over the hermes session store.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ optional_env:
|
|||||||
prompt: "Allow all devices? (true/false)"
|
prompt: "Allow all devices? (true/false)"
|
||||||
password: false
|
password: false
|
||||||
- name: IRIS_PUSH_BACKEND
|
- 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"
|
prompt: "Push backend"
|
||||||
password: false
|
password: false
|
||||||
- name: IRIS_FCM_SERVICE_ACCOUNT
|
- name: IRIS_FCM_SERVICE_ACCOUNT
|
||||||
|
|||||||
+7
-4
@@ -1,7 +1,10 @@
|
|||||||
# 08 — Push Notifications, Outbox & Sync
|
# 08 — Push Notifications, Outbox & Sync
|
||||||
|
|
||||||
The gateway can't reach a sleeping phone directly. Push goes through a cloud
|
The gateway can't reach a sleeping phone directly. Push goes through a cloud
|
||||||
relay. **Decision: FCM primary, ntfy fallback** (`IRIS_PUSH_BACKEND`).
|
relay. **Decision: ntfy default, FCM optional** (`IRIS_PUSH_BACKEND`).
|
||||||
|
Privacy: FCM push metadata (notification title, device token) is routed
|
||||||
|
through Google's servers — for truly private communication use ntfy
|
||||||
|
(self-hosted), which keeps everything on your own infrastructure.
|
||||||
|
|
||||||
## 8.1 When push fires
|
## 8.1 When push fires
|
||||||
|
|
||||||
@@ -54,9 +57,9 @@ class PushBackend(Protocol):
|
|||||||
def configured(self) -> bool: ...
|
def configured(self) -> bool: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Selected at adapter init by `IRIS_PUSH_BACKEND` (`fcm` default, `ntfy`).
|
Selected at adapter init by `IRIS_PUSH_BACKEND` (`ntfy` default, `fcm`).
|
||||||
|
|
||||||
### 8.2.1 `FcmBackend` (primary)
|
### 8.2.1 `FcmBackend` (optional; metadata via Google)
|
||||||
|
|
||||||
- **FCM HTTP v1 API** via `httpx` (core dep). Auth = Firebase **service
|
- **FCM HTTP v1 API** via `httpx` (core dep). Auth = Firebase **service
|
||||||
account** (`IRIS_FCM_SERVICE_ACCOUNT` JSON path) → mint a short-lived
|
account** (`IRIS_FCM_SERVICE_ACCOUNT` JSON path) → mint a short-lived
|
||||||
@@ -74,7 +77,7 @@ Selected at adapter init by `IRIS_PUSH_BACKEND` (`fcm` default, `ntfy`).
|
|||||||
- Batch: FCM allows up to 500 tokens/message; we send per-device (1 user, few
|
- Batch: FCM allows up to 500 tokens/message; we send per-device (1 user, few
|
||||||
devices).
|
devices).
|
||||||
|
|
||||||
### 8.2.2 `NtfyBackend` (fallback, self-host friendly)
|
### 8.2.2 `NtfyBackend` (default, self-host friendly)
|
||||||
|
|
||||||
- Reuses hermes's existing ntfy publish path (hermes ships an ntfy adapter).
|
- Reuses hermes's existing ntfy publish path (hermes ships an ntfy adapter).
|
||||||
- Publish to `NTFY_TOPIC` on `NTFY_SERVER_URL` (default `https://ntfy.sh`) via
|
- Publish to `NTFY_TOPIC` on `NTFY_SERVER_URL` (default `https://ntfy.sh`) via
|
||||||
|
|||||||
@@ -77,15 +77,16 @@ hermes --version # sanity
|
|||||||
4. The app's `FirebaseMessagingService` obtains the FCM token at runtime and
|
4. The app's `FirebaseMessagingService` obtains the FCM token at runtime and
|
||||||
registers it via `hello` / `fcm.register`.
|
registers it via `hello` / `fcm.register`.
|
||||||
|
|
||||||
> Skip Firebase → set `IRIS_PUSH_BACKEND=ntfy` and configure `NTFY_TOPIC` /
|
> Skip Firebase → the default is already ntfy: leave `IRIS_PUSH_BACKEND` unset
|
||||||
> `NTFY_SERVER_URL` (self-host ntfy or use ntfy.sh). See `08-push.md`.
|
> (or set it to `ntfy`) and configure `NTFY_TOPIC` / `NTFY_SERVER_URL`
|
||||||
|
> (self-host ntfy or use ntfy.sh). See `08-push.md`.
|
||||||
|
|
||||||
## 12.6 Environment variables (summary)
|
## 12.6 Environment variables (summary)
|
||||||
|
|
||||||
**Secrets (`~/.hermes/.env`):**
|
**Secrets (`~/.hermes/.env`):**
|
||||||
```
|
```
|
||||||
IRIS_TOKEN=<64-hex>
|
IRIS_TOKEN=<64-hex>
|
||||||
IRIS_PUSH_BACKEND=fcm # or ntfy
|
IRIS_PUSH_BACKEND=ntfy # default; fcm = opt-in (metadata via Google)
|
||||||
IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json
|
IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json
|
||||||
# IRIS_FCM_SERVER_KEY=<legacy key> # fallback if no service account
|
# IRIS_FCM_SERVER_KEY=<legacy key> # fallback if no service account
|
||||||
# NTFY_TOPIC=iris-push # when ntfy
|
# NTFY_TOPIC=iris-push # when ntfy
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
| # | Decision | Choice | Rationale |
|
| # | Decision | Choice | Rationale |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | Desktop app tech | **Compose Multiplatform** | Desktop = "the Android app, tweaked"; share protocol/state/UI. |
|
| 1 | Desktop app tech | **Compose Multiplatform** | Desktop = "the Android app, tweaked"; share protocol/state/UI. |
|
||||||
| 2 | Push backend | **Both — FCM primary, ntfy fallback** | FCM is standard/reliable; ntfy for self-hosters with no Firebase. `IRIS_PUSH_BACKEND`. |
|
| 2 | Push backend | **Both — ntfy default, FCM optional** (issue #10: FCM metadata — title, device token — is routed via Google's servers, contradicting the privacy claim; ntfy keeps it on your own infrastructure) | `IRIS_PUSH_BACKEND`. |
|
||||||
| 3 | Media transport | **Over the WebSocket** | One transport, zero new Python deps; chunked binary frames. |
|
| 3 | Media transport | **Over the WebSocket** | One transport, zero new Python deps; chunked binary frames. |
|
||||||
| 4 | Phone default layout | **User-toggleable, single-pane default** | App-like on phones; auto two-pane on large screens; desktop defaults two-pane. |
|
| 4 | Phone default layout | **User-toggleable, single-pane default** | App-like on phones; auto two-pane on large screens; desktop defaults two-pane. |
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -30,7 +30,7 @@ top-to-bottom once, then use the numbered docs as a lookup while implementing.
|
|||||||
| 5 | [`05-streaming.md`](05-streaming.md) | Streaming / reasoning / tools / intermediate. |
|
| 5 | [`05-streaming.md`](05-streaming.md) | Streaming / reasoning / tools / intermediate. |
|
||||||
| 6 | [`06-channels-cron-search.md`](06-channels-cron-search.md) | Channels, threads, cron delivery, search. |
|
| 6 | [`06-channels-cron-search.md`](06-channels-cron-search.md) | Channels, threads, cron delivery, search. |
|
||||||
| 7 | [`07-media.md`](07-media.md) | Media upload/download + playback. |
|
| 7 | [`07-media.md`](07-media.md) | Media upload/download + playback. |
|
||||||
| 8 | [`08-push.md`](08-push.md) | Push (FCM + ntfy), outbox, sync. |
|
| 8 | [`08-push.md`](08-push.md) | Push (ntfy default + FCM optional), outbox, sync. |
|
||||||
| 9 | [`09-pairing-security.md`](09-pairing-security.md) | Pairing, auth, security model. |
|
| 9 | [`09-pairing-security.md`](09-pairing-security.md) | Pairing, auth, security model. |
|
||||||
| 10 | [`10-android-app.md`](10-android-app.md) | When building the Android app. |
|
| 10 | [`10-android-app.md`](10-android-app.md) | When building the Android app. |
|
||||||
| 11 | [`11-desktop-app.md`](11-desktop-app.md) | When building the Desktop app. |
|
| 11 | [`11-desktop-app.md`](11-desktop-app.md) | When building the Desktop app. |
|
||||||
@@ -47,6 +47,7 @@ Machine-readable / diagrams:
|
|||||||
|
|
||||||
- [`protocol/frames.schema.json`](protocol/frames.schema.json) — wire-frame schema.
|
- [`protocol/frames.schema.json`](protocol/frames.schema.json) — wire-frame schema.
|
||||||
- [`diagrams/architecture.mmd`](diagrams/architecture.mmd) — mermaid architecture.
|
- [`diagrams/architecture.mmd`](diagrams/architecture.mmd) — mermaid architecture.
|
||||||
|
- [`playstore-listing.md`](playstore-listing.md) — Play Store listing text (incl. the FCM/ntfy privacy note).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Play Store Listing
|
||||||
|
|
||||||
|
Copy-paste text for the Play Console (and the F-Droid / sideload pages).
|
||||||
|
Keep this file in sync with the README whenever the privacy story changes.
|
||||||
|
|
||||||
|
## Short description (≤ 80 chars)
|
||||||
|
|
||||||
|
Private chat for your personal AI agent — Android & desktop.
|
||||||
|
|
||||||
|
## Full description
|
||||||
|
|
||||||
|
Iris is a native chat app for your personal AI agent (hermes-agent):
|
||||||
|
streaming replies, visible reasoning, structured tool activity, channels,
|
||||||
|
threads, media, search — Telegram-quality, on your own infrastructure.
|
||||||
|
|
||||||
|
**Absolute Privacy!** — everything stays on your own infrastructure:
|
||||||
|
|
||||||
|
- Your gateway, your machine, your data. No cloud middleman for chat.
|
||||||
|
- **Push notifications:** ntfy by default — push metadata stays on your own
|
||||||
|
(self-hosted) ntfy server.
|
||||||
|
- **FCM is opt-in** (`IRIS_PUSH_BACKEND=fcm`): standard and reliable, but FCM
|
||||||
|
push metadata (notification title, device token) is routed through
|
||||||
|
**Google's servers**. If you want truly private communication, use ntfy
|
||||||
|
(self-hosted) instead — it is the default.
|
||||||
|
|
||||||
|
No file limit. No character limit. Full markdown + HTML artifact preview.
|
||||||
|
All settings live in the app.
|
||||||
|
|
||||||
|
Runs on Android and desktop (Linux, macOS, Windows) from one shared codebase.
|
||||||
|
|
||||||
|
## Privacy note (for the "Data safety" section / FAQ)
|
||||||
|
|
||||||
|
Iris talks directly to your own hermes gateway over a private, token-authenticated
|
||||||
|
connection. By default, push notifications use ntfy, which you can self-host so
|
||||||
|
that push metadata never leaves your infrastructure. If you explicitly enable
|
||||||
|
FCM, push metadata (notification title, device token) is sent via Google's FCM
|
||||||
|
servers; chat content itself is not sent to Google — FCM only carries a short
|
||||||
|
preview, and full content is fetched from your gateway over the authenticated
|
||||||
|
connection. For truly private communication, use the default ntfy backend
|
||||||
|
(self-hosted).
|
||||||
+23
-19
@@ -39,7 +39,8 @@ What it does:
|
|||||||
- Generates `IRIS_TOKEN` (64 hex chars) if none exists and stores it in
|
- Generates `IRIS_TOKEN` (64 hex chars) if none exists and stores it in
|
||||||
`~/.hermes/.env` (it prints the token once, at generation).
|
`~/.hermes/.env` (it prints the token once, at generation).
|
||||||
- Prompts for the WS bind host (default `127.0.0.1`), port (default `8790`),
|
- Prompts for the WS bind host (default `127.0.0.1`), port (default `8790`),
|
||||||
and push backend (`fcm` or `ntfy`, default `fcm`).
|
and push backend (`ntfy` or `fcm`, default `ntfy`); warns when `fcm` is
|
||||||
|
chosen (push metadata via Google's servers).
|
||||||
- Prints the pairing payload (a QR-encodable `iris://pair?host=…&port=…&token=…`
|
- Prints the pairing payload (a QR-encodable `iris://pair?host=…&port=…&token=…`
|
||||||
string), a scannable QR of that payload, and the server URL
|
string), a scannable QR of that payload, and the server URL
|
||||||
(`ws://<host>:8790/ws`).
|
(`ws://<host>:8790/ws`).
|
||||||
@@ -100,26 +101,10 @@ Push wakes a backgrounded/offline device; on reconnect the app syncs the
|
|||||||
outbox, so nothing is lost. Push fires when the device is offline, plus for
|
outbox, so nothing is lost. Push fires when the device is offline, plus for
|
||||||
high-priority events (approvals, clarifies, cron) even when a device is live.
|
high-priority events (approvals, clarifies, cron) even when a device is live.
|
||||||
|
|
||||||
### FCM (default; needs a Firebase project)
|
### ntfy (default; zero-config)
|
||||||
|
|
||||||
1. Create a Firebase project (console.firebase.google.com) and add an Android
|
|
||||||
app with the app's applicationId; download `google-services.json` into
|
|
||||||
`app/androidApp/`.
|
|
||||||
2. Create a service account (Project settings → Service accounts → Generate new
|
|
||||||
private key) and store the JSON path in `~/.hermes/.env`:
|
|
||||||
`IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json`.
|
|
||||||
3. Keep `IRIS_PUSH_BACKEND=fcm` (the default).
|
|
||||||
|
|
||||||
Without a Firebase project the FCM path is **inert** (the app's FCM service
|
|
||||||
does nothing) — use ntfy below, or add Firebase later.
|
|
||||||
|
|
||||||
**What you see:** system notifications for new messages when the app is
|
|
||||||
backgrounded; tapping one deep-links to the chat.
|
|
||||||
|
|
||||||
### ntfy (zero-config fallback)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
IRIS_PUSH_BACKEND=ntfy
|
IRIS_PUSH_BACKEND=ntfy # the default — can be left unset
|
||||||
```
|
```
|
||||||
|
|
||||||
- The device **generates its own topic** automatically (no `NTFY_TOPIC` needed);
|
- The device **generates its own topic** automatically (no `NTFY_TOPIC` needed);
|
||||||
@@ -128,10 +113,29 @@ IRIS_PUSH_BACKEND=ntfy
|
|||||||
recommended** — the public ntfy.sh SSE endpoint is flaky (it has served its
|
recommended** — the public ntfy.sh SSE endpoint is flaky (it has served its
|
||||||
web UI instead of the stream), while a self-hosted instance gives reliable
|
web UI instead of the stream), while a self-hosted instance gives reliable
|
||||||
SSE. For a real trust boundary use a private topic + `NTFY_AUTH_TOKEN`.
|
SSE. For a real trust boundary use a private topic + `NTFY_AUTH_TOKEN`.
|
||||||
|
- **Privacy:** ntfy keeps push metadata (title, topic) on your own
|
||||||
|
infrastructure — this is the backend for truly private communication.
|
||||||
|
|
||||||
**What you see:** a low-priority foreground "ntfy listener" notification while
|
**What you see:** a low-priority foreground "ntfy listener" notification while
|
||||||
the app is off; incoming pushes trigger a silent sync.
|
the app is off; incoming pushes trigger a silent sync.
|
||||||
|
|
||||||
|
### FCM (opt-in; needs a Firebase project)
|
||||||
|
|
||||||
|
> **Privacy note:** FCM push metadata (notification title, device token) is
|
||||||
|
> routed through **Google's servers**. If you want truly private
|
||||||
|
> communication, use ntfy (self-hosted) instead — it is the default.
|
||||||
|
|
||||||
|
1. Create a Firebase project (console.firebase.google.com) and add an Android
|
||||||
|
app with the app's applicationId; download `google-services.json` into
|
||||||
|
`app/androidApp/`.
|
||||||
|
2. Create a service account (Project settings → Service accounts → Generate new
|
||||||
|
private key) and store the JSON path in `~/.hermes/.env`:
|
||||||
|
`IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json`.
|
||||||
|
3. Set `IRIS_PUSH_BACKEND=fcm`.
|
||||||
|
|
||||||
|
Without a Firebase project the FCM path is **inert** (the app's FCM service
|
||||||
|
does nothing) — use ntfy (the default), or add Firebase later.
|
||||||
|
|
||||||
## 5. Remote access
|
## 5. Remote access
|
||||||
|
|
||||||
- **Tailscale / WireGuard (recommended):** the gateway gets a stable tailnet IP;
|
- **Tailscale / WireGuard (recommended):** the gateway gets a stable tailnet IP;
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ register the (delivery-validated) file in the media registry and emit
|
|||||||
``validate_media_delivery_path`` at pull time.
|
``validate_media_delivery_path`` at pull time.
|
||||||
|
|
||||||
Milestone M5: push + offline. Frames with no live subscriber are parked in
|
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
|
the outbox (M3) AND wake the device via the push backend (``push.py``: ntfy
|
||||||
HTTP v1 primary, ntfy fallback, selected by ``IRIS_PUSH_BACKEND``).
|
default, FCM HTTP v1 as an option, selected by ``IRIS_PUSH_BACKEND``).
|
||||||
``notification`` frames render in-app banners and mirror to push (channel
|
``notification`` frames render in-app banners and mirror to push (channel
|
||||||
events, cron deliveries, approvals, clarifies); high-priority kinds push even
|
events, cron deliveries, approvals, clarifies); high-priority kinds push even
|
||||||
when a device is live. ``fcm.register`` rotates push tokens (registry + live
|
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_HTTP_PORT = 8791 # docs/19: HTTP fallback leg
|
||||||
DEFAULT_HOME_CHANNEL = "default"
|
DEFAULT_HOME_CHANNEL = "default"
|
||||||
DEFAULT_HOME_CHANNEL_NAME = "Default"
|
DEFAULT_HOME_CHANNEL_NAME = "Default"
|
||||||
DEFAULT_PUSH_BACKEND = "fcm"
|
DEFAULT_PUSH_BACKEND = "ntfy"
|
||||||
DEFAULT_OUTBOX_RETENTION_HOURS = 72
|
DEFAULT_OUTBOX_RETENTION_HOURS = 72
|
||||||
DEFAULT_MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # 100 MB
|
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)))
|
save_env_value("IRIS_HTTP_PORT", str(_parse_port(port)))
|
||||||
backend = prompt(
|
backend = prompt(
|
||||||
"Push backend (fcm/ntfy)",
|
"Push backend (ntfy/fcm)",
|
||||||
default=get_env_value("IRIS_PUSH_BACKEND") or DEFAULT_PUSH_BACKEND,
|
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).
|
# 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
|
# 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
|
# /fast): picker_id -> pending state. In-memory only — a gateway
|
||||||
# restart expires them (a stale picker.select is a no-op).
|
# restart expires them (a stale picker.select is a no-op).
|
||||||
self._pending_pickers: dict[str, dict] = {}
|
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.
|
# the outbox-prune banner.
|
||||||
self._push: PushBackend = build_push_backend(
|
self._push: PushBackend = build_push_backend(
|
||||||
self.push_backend,
|
self.push_backend,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ optional_env:
|
|||||||
prompt: "Allow all devices? (true/false)"
|
prompt: "Allow all devices? (true/false)"
|
||||||
password: false
|
password: false
|
||||||
- name: IRIS_PUSH_BACKEND
|
- 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"
|
prompt: "Push backend"
|
||||||
password: false
|
password: false
|
||||||
- name: IRIS_FCM_SERVICE_ACCOUNT
|
- 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:
|
``PushBackend`` interface with two implementations:
|
||||||
- ``FcmBackend``: FCM HTTP v1 via ``httpx`` + a Firebase service account
|
- ``FcmBackend``: FCM HTTP v1 via ``httpx`` + a Firebase service account
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
(default ``https://ntfy.sh``) via ``httpx``; the app's listener
|
(default ``https://ntfy.sh``) via ``httpx``; the app's listener
|
||||||
subscribes to the topic.
|
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
|
Fired when a frame has no live subscriber; the data payload drives a silent
|
||||||
sync on the device (docs/08-push.md).
|
sync on the device (docs/08-push.md).
|
||||||
|
|
||||||
@@ -33,7 +33,9 @@ import httpx
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
FCM_SCOPE = "https://www.googleapis.com/auth/firebase.messaging"
|
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_V1_SEND_URL = "https://fcm.googleapis.com/v1/projects/{project_id}/messages:send"
|
||||||
FCM_LEGACY_SEND_URL = "https://fcm.googleapis.com/fcm/send"
|
FCM_LEGACY_SEND_URL = "https://fcm.googleapis.com/fcm/send"
|
||||||
# Refresh the cached access token this long before its expiry.
|
# Refresh the cached access token this long before its expiry.
|
||||||
@@ -54,7 +56,7 @@ class PushBackend:
|
|||||||
name: str = "push"
|
name: str = "push"
|
||||||
# DeviceRegistry column that carries this backend's target token.
|
# DeviceRegistry column that carries this backend's target token.
|
||||||
# Not a secret: a DB column name (string literal), not a credential.
|
# 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 = ""
|
token_field: str = ""
|
||||||
|
|
||||||
def configured(self) -> bool:
|
def configured(self) -> bool:
|
||||||
@@ -87,8 +89,8 @@ class FcmBackend(PushBackend):
|
|||||||
|
|
||||||
name = "fcm"
|
name = "fcm"
|
||||||
# Not a secret: a DB column name (string literal), not a credential.
|
# Not a secret: a DB column name (string literal), not a credential.
|
||||||
# pi-lens-ignore: python-hardcoded-secrets
|
# pi-lens-ignore: S105
|
||||||
token_field = "fcm_token"
|
token_field = "fcm_token" # noqa: S105
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -257,8 +259,8 @@ class NtfyBackend(PushBackend):
|
|||||||
|
|
||||||
name = "ntfy"
|
name = "ntfy"
|
||||||
# Not a secret: a DB column name (string literal), not a credential.
|
# Not a secret: a DB column name (string literal), not a credential.
|
||||||
# pi-lens-ignore: python-hardcoded-secrets
|
# pi-lens-ignore: S105
|
||||||
token_field = "ntfy_topic"
|
token_field = "ntfy_topic" # noqa: S105
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -332,9 +334,14 @@ def build_push_backend(
|
|||||||
ntfy_server_url: str | None = None,
|
ntfy_server_url: str | None = None,
|
||||||
ntfy_auth_token: str | None = None,
|
ntfy_auth_token: str | None = None,
|
||||||
) -> PushBackend:
|
) -> PushBackend:
|
||||||
"""Select the backend by name (``IRIS_PUSH_BACKEND``; fcm default)."""
|
"""Select the backend by name (``IRIS_PUSH_BACKEND``; ntfy default).
|
||||||
if (name or "").strip().lower() == "ntfy":
|
|
||||||
return NtfyBackend(
|
ntfy is the default: it keeps push metadata on your own infrastructure.
|
||||||
topic=ntfy_topic, server_url=ntfy_server_url, auth_token=ntfy_auth_token
|
FCM is opt-in (``IRIS_PUSH_BACKEND=fcm``) — its metadata (title, device
|
||||||
)
|
token) is routed through Google's servers.
|
||||||
return FcmBackend(service_account=fcm_service_account, server_key=fcm_server_key)
|
"""
|
||||||
|
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
|
push = plugin.push
|
||||||
assert isinstance(push.build_push_backend("fcm"), push.FcmBackend)
|
assert isinstance(push.build_push_backend("fcm"), push.FcmBackend)
|
||||||
assert isinstance(push.build_push_backend("ntfy"), push.NtfyBackend)
|
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 isinstance(push.build_push_backend(" NTFY "), push.NtfyBackend)
|
||||||
|
|
||||||
assert push.build_push_backend("ntfy", ntfy_topic="my-topic").configured() is True
|
assert push.build_push_backend("ntfy", ntfy_topic="my-topic").configured() is True
|
||||||
|
|||||||
Reference in new issue
Block a user