feat(integration): incoming webhooks (post-as URL) (LC-74) #170
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-74-incoming-webhooks"
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?
Summary
Incoming webhooks (LC-74). A room moderator creates a secret URL; any external system POSTs JSON to it and the message appears in the room attributed to a synthetic webhook actor (not a real user). Builds on the merged LC-72 secret-hashing pattern.
Design
messages.webhook_id(nullable, migration0041). Webhook rows storeuser_id = ''+webhook_id-messages.user_idhas no cross-db FK, so an empty id is safe and avoids making the columnOptionacross the whole codebase.RawMessage/Message/MessageViewcarrywebhook_id; a newresolve_msg_authorbranches on it (returns the webhook's name/avatar viaAuthorMeta { is_webhook, avatar_url }). All 9 MessageView build sites + the WS broadcast path go through it. Template renders awebhookbadge, drops the DM link, uses the avatar URL (or initials).incoming_webhooks, chat.db):room_idFKON DELETE CASCADE, name, optional avatar URL, only an HMAC of the secret (keyed byLETS_CHAT_SECRET_KEY).revoked_atretains the row for audit.POST /webhook/{secret}: unauthenticated, merged after TraceLayer so the secret never hits request logs. Body{"text": "...", "markdown": bool};markdown:truerenders through the LC-59 pipeline, else escaped to literal. 401 unknown, 410 revoked, 429 +Retry-Afterpast the per-webhook cap (60/min), 204 success. Broadcasts + fans out@mentions.GET/POST /room/{id}/webhooks(create reveals URL once),POST .../{wid}/revoke, linked from the room Moderators page.docs/api.mddocuments it.Acceptance criteria
Retry-After."markdown": true.Tests
routes_webhooks.rs: create+attribute (badge render), unknown-secret 401, revoke 410, rate-limit 429 + Retry-After, markdown flag, room-delete cascade. Migration0041appended to all hand-rolled chat lists.just check,just test,just test-saasgreen.🤖 Generated with Claude Code
A room moderator creates an incoming webhook for a room; any external system POSTs JSON to its secret URL and it appears as a message attributed to a synthetic webhook actor, not a real user. Synthetic actor: messages gain a nullable webhook_id (migration 0041); webhook messages store user_id='' + webhook_id (messages.user_id has no cross-db FK, so an empty id is safe and avoids making the column Option across the whole codebase). RawMessage/Message/MessageView carry webhook_id; the 9 MessageView build sites resolve author identity through a new resolve_msg_author that branches on webhook_id, returning the webhook's name/avatar (AuthorMeta gains is_webhook + avatar_url). The message template renders a "webhook" badge, suppresses the DM link, and uses the webhook avatar URL (or initials). Storage (incoming_webhooks, chat.db): room_id FK ON DELETE CASCADE (deleting a room removes its webhooks), name, optional avatar_url, and only an HMAC of the secret (keyed by LETS_CHAT_SECRET_KEY, like API tokens) - a chat.db leak cannot reconstruct usable URLs. revoked_at retains the row for audit. Public route POST /webhook/{secret}: unauthenticated (the secret is the credential), merged AFTER the TraceLayer so the secret never lands in request logs (only the webhook id is logged from the handler). Body {"text": "...", "markdown": bool}; markdown:true renders through the LC-59 pipeline, otherwise the text is escaped to render literally. 401 unknown secret, 410 revoked, 429 + Retry-After past the per-webhook cap (60/min via a new RateLimitKind::Webhook), 204 on success. Webhook messages broadcast through the room hub and fan out @mentions (no author to exclude). Management UI (cookie-authed, room-moderator gated): GET/POST /room/{id}/webhooks (create reveals the URL once) and POST /room/{id}/webhooks/{wid}/revoke, linked from the room Moderators page. docs/api.md documents the endpoint, payload, and status codes. Tests: routes_webhooks.rs covers create+attribute, unknown-secret 401, revoke 410, rate-limit 429 + Retry-After, the markdown flag, and room-delete cascade. Migration 0041 appended to every hand-rolled chat migration list. just check, just test, just test-saas all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>The public POST /webhook/{secret} endpoint is unauthenticated, so bound a single message to 16 KiB (generous for alerts) and return 400 on oversize, rather than relying solely on Axum's 2 MiB default body limit. New test covers the 400. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>