Private
Public Access
Add URL previews for chat messages (#43)
Slack/Discord-style link unfurling: the first http(s) URL in a message's content gets a small preview card (title/description/image/site name) fetched from the page's Open Graph tags. Backend: - Message.preview_url (extracted at create/edit time, cheap regex, no I/O) points at a link_previews cache row keyed by URL -- the same URL posted in different messages/rooms fetches once, and a failed fetch is cached too so a dead URL isn't retried on every reference. - The actual fetch runs in a background asyncio.create_task from broadcast_new_message/broadcast_message_update, on its own DB session, so a slow third-party site never delays message delivery. A separate "link_preview" WS envelope carries the result once it resolves. - SSRF protection reuses app/services/ssrf.py's validate_target_url (renamed from UnsafeWebhookUrlError to UnsafeUrlError now that it's shared with webhooks), but re-validates before every hop of a redirect chain rather than once up front -- redirects are followed manually so each intermediate URL is checked before it's ever connected to. - Parsed with stdlib html.parser -- no new dependency. Frontend: a LinkPreviewCard rendered under message content when present, patched into state live via the new WS envelope and included in message history for reloads. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
752da74c5a
commit
760d2cf5dd
18 files changed
+835
-21
No files matched your search
@@ -122,16 +122,45 @@ export function ChatPane({
|
||||
setLive((prev) => [...prev, envelope])
|
||||
markRead()
|
||||
} else if (envelope.type === 'message_update' && envelope.room_id === room.id) {
|
||||
// link_preview only survives the edit if the URL it came from is
|
||||
// still there -- an edit that changed or removed it clears the
|
||||
// stale preview instead of leaving the old one showing. A new one
|
||||
// (if the new URL has any) arrives via its own 'link_preview'
|
||||
// envelope shortly after, same as a fresh send.
|
||||
setHistory((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === envelope.id ? { ...m, content: envelope.content, edited_at: envelope.edited_at } : m,
|
||||
m.id === envelope.id
|
||||
? {
|
||||
...m,
|
||||
content: envelope.content,
|
||||
edited_at: envelope.edited_at,
|
||||
link_preview: m.link_preview?.url === envelope.preview_url ? m.link_preview : null,
|
||||
}
|
||||
: m,
|
||||
),
|
||||
)
|
||||
setLive((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === envelope.id ? { ...m, content: envelope.content, edited_at: envelope.edited_at } : m,
|
||||
m.id === envelope.id
|
||||
? {
|
||||
...m,
|
||||
content: envelope.content,
|
||||
edited_at: envelope.edited_at,
|
||||
link_preview: m.link_preview?.url === envelope.preview_url ? m.link_preview : null,
|
||||
}
|
||||
: m,
|
||||
),
|
||||
)
|
||||
} else if (envelope.type === 'link_preview' && envelope.room_id === room.id) {
|
||||
const linkPreview = {
|
||||
url: envelope.url,
|
||||
title: envelope.title,
|
||||
description: envelope.description,
|
||||
image_url: envelope.image_url,
|
||||
site_name: envelope.site_name,
|
||||
}
|
||||
setHistory((prev) => prev.map((m) => (m.id === envelope.id ? { ...m, link_preview: linkPreview } : m)))
|
||||
setLive((prev) => prev.map((m) => (m.id === envelope.id ? { ...m, link_preview: linkPreview } : m)))
|
||||
} else if (envelope.type === 'reaction_update' && envelope.room_id === room.id) {
|
||||
setHistory((prev) =>
|
||||
prev.map((m) => (m.id === envelope.id ? { ...m, reactions: envelope.reactions } : m)),
|
||||
|
||||
Reference in new issue
Block a user