feat: push notifications via the push gateway (#4)#80
Merged
Conversation
…33) NullFeed had live WebSocket events for an open app but no way to reach a backgrounded/closed one. Integrate the shared push gateway (push.julian.codes) so new-episode notifications land as real iOS pushes, modeled on Cantinarr's gateway client. No Apple push key of our own is required. Config (app/config.py): NULLFEED_PUSH_GATEWAY_URL (default https://push.julian.codes), NULLFEED_PUSH_API_KEY, NULLFEED_PUSH_ENROLL_TOKEN. A blank gateway URL disables push entirely (every path no-ops). Documented in .env.example. Gateway client (app/services/push_gateway.py): a sync httpx core (enroll, register_device, unregister_device, send_to_users) with async wrappers (asyncio.to_thread) for the FastAPI endpoints. Topic is always codes.julian.nullfeed; tenant keys target user_ids (the gateway rejects raw tokens). Zero-config auto-enrollment on first use: with no explicit key set we self-enroll and persist the issued pgk_ key durably under config_path as a 0600 file, claimed with the same atomic hard-link dance as app/utils/tickets.py, so it survives restarts and is shared by every worker. Everything is best-effort: a disabled/unreachable gateway or a per-call error is logged and swallowed, never raised into the poller or an endpoint. Endpoints (app/api/push.py, mounted in app/main.py), session-authenticated: - POST /api/push/register {device_token, device_id?, platform?=ios} -> forwards to gateway POST /v1/devices with the caller's user id + topic. - DELETE /api/push/register {device_id | device_token, platform?} -> forwards a gateway DELETE /v1/devices. Both return {"enabled": false} (200) when push is disabled rather than erroring. Send-on-new-episode: channel_poller._emit_new_episode_events now sends a push (best-effort, sync) alongside the existing WS publish, per (subscriber, new video): to.user_ids=[user_id], notification {title: channel name, body: video title}, data {type:"new_episode", video_id} for client deep-linking. A push failure never breaks polling. Persistence is a file (no DB table / migration). Tests (mock httpx, no network): auto-enroll persists + reuses the key (and sends X-Enroll-Token when gated), register forwards user.id + token + topic, a new episode triggers a per- subscriber send with the right to.user_ids + data.video_id, push DISABLED no-ops the endpoints and the poller (no call-out), and a gateway error during a send does not break the poll. Suite: 239 passed (15 new). ruff check + format clean; mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMKM1rDWn8wNh93MMUtxY
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
New-episode push notifications (roadmap LATER tier, #4), integrated with the public push gateway (push.julian.codes) the same way Cantinarr does. Additive — no migration; disabled unless a gateway URL is configured (it defaults to the public relay).
Config (
app/config.py,.env.example)NULLFEED_PUSH_GATEWAY_URL(defaulthttps://push.julian.codes),NULLFEED_PUSH_API_KEY(default ""),NULLFEED_PUSH_ENROLL_TOKEN(default ""). Blank URL ⇒ push fully disabled (every path no-ops).Gateway client (
app/services/push_gateway.py)POST /v1/enroll), persisting the issuedpgk_key as a 0600 file underconfig_pathvia the same atomic hard-link claim as the ticket secret (survives restart, shared across workers). ExplicitNULLFEED_PUSH_API_KEYwins and isn't persisted.register_device/unregister_device(POST/DELETE /v1/devices, topiccodes.julian.nullfeed),send_to_users(POST /v1/notifications,to.user_idsonly — tenant keys can't target raw tokens). Sync core (for the Celery poller) + async wrappers viaasyncio.to_thread(for endpoints). Best-effort throughout: disabled/unreachable/error is logged and swallowed, never raised into the poller or an endpoint.Endpoints (
app/api/push.py, X-User-Token auth)POST /api/push/register {device_token, device_id?, platform?="ios"}→ forwards to the gateway withuser.id;DELETE /api/push/register {device_id|device_token}. Push disabled ⇒200 {"enabled": false}.Send on new episode
channel_poller._emit_new_episode_events(sync Celery, after the back-catalog gate + WS publish) sends per subscriber×new video:to.user_ids=[user_id], alert title=channel/body=video title,data {"type":"new_episode","video_id"}for deep-linking, priority high. A push error never breaks polling.Verification (local)
pytest -q→ 239 passed (+15, httpx mocked: auto-enroll persists+reuses, register forwards user.id+token+topic, new_episode→send with rightto.user_ids+data.video_id, disabled no-ops, gateway error doesn't break the poll).ruff+mypyclean · no migration.Clients next: APNs registration →
POST /api/push/register; tap ondata.video_id→/player/:id. Needs you (I'll handle with your access): confirm the gateway's enrollment mode (auto-enroll assumesopen; else an enroll token / explicit key) and the Apple Push capability oncodes.julian.nullfeed.🤖 Generated with Claude Code
https://claude.ai/code/session_01RXMKM1rDWn8wNh93MMUtxY