diff --git a/src/app/api/users/[handle]/posts/route.ts b/src/app/api/users/[handle]/posts/route.ts index 50115cf..8a210ca 100644 --- a/src/app/api/users/[handle]/posts/route.ts +++ b/src/app/api/users/[handle]/posts/route.ts @@ -42,38 +42,35 @@ const extractTextAndUrls = (value?: string | null) => { const normalizeUrl = (value: string) => value.replace(/[)\].,!?]+$/, ''); -const fetchLinkPreview = async (url: string) => { +const fetchLinkPreview = async (url: string, origin: string) => { try { - const res = await fetch(url, { - headers: { - 'User-Agent': 'SynapsisBot/1.0', - }, + const previewUrl = new URL('/api/media/preview', origin); + previewUrl.searchParams.set('url', url); + const res = await fetch(previewUrl.toString(), { + headers: { 'Accept': 'application/json' }, signal: AbortSignal.timeout(4000), }); if (!res.ok) return null; - const html = await res.text(); - const getMeta = (property: string) => { - const regex = new RegExp(`]+(?:property|name)=["'](?:og:)?${property}["'][^>]+content=["']([^"']+)["']`, 'i'); - const match = html.match(regex); - if (match) return match[1]; - const regexRev = new RegExp(`]+content=["']([^"']+)["'][^>]+(?:property|name)=["'](?:og:)?${property}["']`, 'i'); - const matchRev = html.match(regexRev); - return matchRev ? matchRev[1] : null; - }; - const title = getMeta('title') || html.match(/([^<]+)<\/title>/i)?.[1]; - const description = getMeta('description'); - const image = getMeta('image'); + const data = await res.json(); return { - url, - title: title?.trim() || url, - description: description?.trim() || null, - image: image?.trim() || null, + url: data?.url || url, + title: data?.title || null, + description: data?.description || null, + image: data?.image || null, }; } catch { return null; } }; +const stripFirstUrl = (text: string, url: string) => { + const idx = text.indexOf(url); + if (idx === -1) return text; + const before = text.slice(0, idx).trimEnd(); + const after = text.slice(idx + url.length).trimStart(); + return `${before} ${after}`.trim(); +}; + const parseRemoteHandle = (handle: string) => { const clean = handle.toLowerCase().replace(/^@/, ''); const parts = clean.split('@').filter(Boolean); @@ -136,6 +133,7 @@ export async function GET(request: Request, context: RouteContext) { bio: sanitizeText(remoteProfile.summary), }; const posts = []; + const origin = new URL(request.url).origin; for (const item of outboxItems) { const activity = item?.type === 'Create' ? item : null; const object = activity?.object; @@ -145,10 +143,11 @@ export async function GET(request: Request, context: RouteContext) { const attachments = Array.isArray(object.attachment) ? object.attachment : []; const { text, urls } = extractTextAndUrls(object.content); const normalizedUrl = urls.length > 0 ? normalizeUrl(urls[0]) : null; - const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl) : null; + const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl, origin) : null; + const contentText = linkPreview && normalizedUrl ? stripFirstUrl(text, normalizedUrl) : text; posts.push({ id: object.id || activity.id, - content: text || '', + content: contentText || '', createdAt: object.published || activity.published || new Date().toISOString(), likesCount: 0, repostsCount: 0, @@ -162,7 +161,7 @@ export async function GET(request: Request, context: RouteContext) { altText: sanitizeText(attachment.name) || null, })), linkPreviewUrl: linkPreview?.url || normalizedUrl, - linkPreviewTitle: linkPreview?.title || (normalizedUrl ?? null), + linkPreviewTitle: linkPreview?.title || null, linkPreviewDescription: linkPreview?.description || null, linkPreviewImage: linkPreview?.image || null, }); @@ -194,6 +193,7 @@ export async function GET(request: Request, context: RouteContext) { bio: sanitizeText(remoteProfile.summary), }; const posts = []; + const origin = new URL(request.url).origin; for (const item of outboxItems) { const activity = item?.type === 'Create' ? item : null; const object = activity?.object; @@ -203,10 +203,11 @@ export async function GET(request: Request, context: RouteContext) { const attachments = Array.isArray(object.attachment) ? object.attachment : []; const { text, urls } = extractTextAndUrls(object.content); const normalizedUrl = urls.length > 0 ? normalizeUrl(urls[0]) : null; - const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl) : null; + const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl, origin) : null; + const contentText = linkPreview && normalizedUrl ? stripFirstUrl(text, normalizedUrl) : text; posts.push({ id: object.id || activity.id, - content: text || '', + content: contentText || '', createdAt: object.published || activity.published || new Date().toISOString(), likesCount: 0, repostsCount: 0, @@ -220,7 +221,7 @@ export async function GET(request: Request, context: RouteContext) { altText: sanitizeText(attachment.name) || null, })), linkPreviewUrl: linkPreview?.url || normalizedUrl, - linkPreviewTitle: linkPreview?.title || (normalizedUrl ?? null), + linkPreviewTitle: linkPreview?.title || null, linkPreviewDescription: linkPreview?.description || null, linkPreviewImage: linkPreview?.image || null, }); diff --git a/src/app/globals.css b/src/app/globals.css index 78707e0..80bfcb7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1357,6 +1357,7 @@ a.btn-primary:visited { .link-preview-card { display: block; margin-top: 12px; + margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; @@ -1543,4 +1544,4 @@ a.btn-primary:visited { .thread-line { margin-left: 20px; border-left: 1px solid var(--border); -} \ No newline at end of file +}