feat(client): per-room RSS/Atom + iCal feeds, token-gated (LC-102) #232
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-102-room-feeds"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A read-only RSS/Atom + iCal feed per room so external tools (RSS readers, calendar apps) can follow a room without a session. The token in the feed URL is the only credential - same posture as the LC-74 incoming webhook.
Spec-review choices (the three recommended defaults, confirmed)
closes_at; no free-text date-phrase parsing.What lands
chat.dbroom_feeds, migration 0053):room_id(FK cascade),kindCHECKrss/ical,token_hashUNIQUE,created_by,created_at,last_fetched_at,revoked_at. Only the HMAC of the token is stored (crate::auth::hash_api_token), so the feature is gated onLETS_CHAT_SECRET_KEY, like webhooks.GET /feed/rss/{token}-> Atom 1.0 of the room's recent messages (text + permalink),application/atom+xml.GET /feed/ical/{token}->VCALENDARwith oneVEVENTper scheduled poll,text/calendar.is_room_accessible). A revoke, kick, room delete, or deleted account -> 410. Unknown token, or a token used on the wrong kind's path -> 404.ETagfingerprints the rendered set; a matchingIf-None-Match-> 304.Cache-Control: private, max-age=300./room/{id}/feeds(moderator-gated) mints an RSS or iCal feed and reveals the URL once, lists feeds (kind / created / last-fetched / status), and revokes; linked from the room moderators page.Acceptance criteria
ETagset; conditional GET returns 304 when nothing changed.Verification
server/tests/routes_feeds.rs(7 tests): mint -> valid Atom/iCal, revoke -> 410, creator-without-access -> 410, deleted-creator -> 410, wrong-kind/unknown -> 404, conditional GET -> 304.just test+just test-saasgreen;cargo fmt --all+ clippy clean.A read-only feed per room so external tools (RSS readers, calendar apps) can follow a room without a session. The token in the feed URL is the only credential, mirroring the LC-74 incoming-webhook posture. Schema: chat.db room_feeds (migration 0053) - room_id (FK cascade), kind CHECK rss/ical, token_hash UNIQUE, created_by, created_at, last_fetched_at, revoked_at. Only the HMAC of the token is stored (crate::auth::hash_api_token), so the feature is gated on LETS_CHAT_SECRET_KEY just like webhooks. Public routes (unauthenticated, merged after the TraceLayer so the token never logs): GET /feed/rss/{token} returns an Atom 1.0 document of the room's recent messages (message text + a permalink back to each message; no media); GET /feed/ical/{token} returns a VCALENDAR with one VEVENT per poll that carries a closes_at. Each fetch re-resolves the feed's creator and re-checks their current room access (db::chat::is_room_accessible), so a revoke, a kick, a room delete, or a deleted account makes the feed return 410. An unknown token, or a token used on the wrong kind's path, returns 404. ETag fingerprints the rendered set; a matching If-None-Match returns 304. Atom is XML-escaped, iCal is RFC 5545 TEXT-escaped with CRLF. Admin surface: /room/{id}/feeds (moderator-gated) mints an RSS or iCal feed and reveals the URL once, lists feeds with last-fetched + status, and revokes; linked from the room moderators page next to webhooks / email-inboxes. Spec-review choices taken (the three recommended defaults): message text + permalink only (no media enclosures); iCal VEVENTs come only from polls with a closes_at (no free-text date parsing); the per-user cross-room mentions feed is out of scope (tracked with LC-82). Tests: server/tests/routes_feeds.rs covers mint -> valid Atom/iCal, revoke -> 410, creator-without-access -> 410, deleted-creator -> 410, wrong-kind/unknown -> 404, and conditional GET -> 304. just test + just test-saas green; cargo fmt --all + clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>