External link enhancment

This commit is contained in:
Christopher
2026-01-22 15:55:57 -08:00
parent 482acf1ff6
commit 2df45e164a
2 changed files with 30 additions and 28 deletions
+28 -27
View File
@@ -42,38 +42,35 @@ const extractTextAndUrls = (value?: string | null) => {
const normalizeUrl = (value: string) => value.replace(/[)\].,!?]+$/, ''); const normalizeUrl = (value: string) => value.replace(/[)\].,!?]+$/, '');
const fetchLinkPreview = async (url: string) => { const fetchLinkPreview = async (url: string, origin: string) => {
try { try {
const res = await fetch(url, { const previewUrl = new URL('/api/media/preview', origin);
headers: { previewUrl.searchParams.set('url', url);
'User-Agent': 'SynapsisBot/1.0', const res = await fetch(previewUrl.toString(), {
}, headers: { 'Accept': 'application/json' },
signal: AbortSignal.timeout(4000), signal: AbortSignal.timeout(4000),
}); });
if (!res.ok) return null; if (!res.ok) return null;
const html = await res.text(); const data = await res.json();
const getMeta = (property: string) => {
const regex = new RegExp(`<meta[^>]+(?:property|name)=["'](?:og:)?${property}["'][^>]+content=["']([^"']+)["']`, 'i');
const match = html.match(regex);
if (match) return match[1];
const regexRev = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["'](?:og:)?${property}["']`, 'i');
const matchRev = html.match(regexRev);
return matchRev ? matchRev[1] : null;
};
const title = getMeta('title') || html.match(/<title>([^<]+)<\/title>/i)?.[1];
const description = getMeta('description');
const image = getMeta('image');
return { return {
url, url: data?.url || url,
title: title?.trim() || url, title: data?.title || null,
description: description?.trim() || null, description: data?.description || null,
image: image?.trim() || null, image: data?.image || null,
}; };
} catch { } catch {
return null; 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 parseRemoteHandle = (handle: string) => {
const clean = handle.toLowerCase().replace(/^@/, ''); const clean = handle.toLowerCase().replace(/^@/, '');
const parts = clean.split('@').filter(Boolean); const parts = clean.split('@').filter(Boolean);
@@ -136,6 +133,7 @@ export async function GET(request: Request, context: RouteContext) {
bio: sanitizeText(remoteProfile.summary), bio: sanitizeText(remoteProfile.summary),
}; };
const posts = []; const posts = [];
const origin = new URL(request.url).origin;
for (const item of outboxItems) { for (const item of outboxItems) {
const activity = item?.type === 'Create' ? item : null; const activity = item?.type === 'Create' ? item : null;
const object = activity?.object; 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 attachments = Array.isArray(object.attachment) ? object.attachment : [];
const { text, urls } = extractTextAndUrls(object.content); const { text, urls } = extractTextAndUrls(object.content);
const normalizedUrl = urls.length > 0 ? normalizeUrl(urls[0]) : null; 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({ posts.push({
id: object.id || activity.id, id: object.id || activity.id,
content: text || '', content: contentText || '',
createdAt: object.published || activity.published || new Date().toISOString(), createdAt: object.published || activity.published || new Date().toISOString(),
likesCount: 0, likesCount: 0,
repostsCount: 0, repostsCount: 0,
@@ -162,7 +161,7 @@ export async function GET(request: Request, context: RouteContext) {
altText: sanitizeText(attachment.name) || null, altText: sanitizeText(attachment.name) || null,
})), })),
linkPreviewUrl: linkPreview?.url || normalizedUrl, linkPreviewUrl: linkPreview?.url || normalizedUrl,
linkPreviewTitle: linkPreview?.title || (normalizedUrl ?? null), linkPreviewTitle: linkPreview?.title || null,
linkPreviewDescription: linkPreview?.description || null, linkPreviewDescription: linkPreview?.description || null,
linkPreviewImage: linkPreview?.image || null, linkPreviewImage: linkPreview?.image || null,
}); });
@@ -194,6 +193,7 @@ export async function GET(request: Request, context: RouteContext) {
bio: sanitizeText(remoteProfile.summary), bio: sanitizeText(remoteProfile.summary),
}; };
const posts = []; const posts = [];
const origin = new URL(request.url).origin;
for (const item of outboxItems) { for (const item of outboxItems) {
const activity = item?.type === 'Create' ? item : null; const activity = item?.type === 'Create' ? item : null;
const object = activity?.object; 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 attachments = Array.isArray(object.attachment) ? object.attachment : [];
const { text, urls } = extractTextAndUrls(object.content); const { text, urls } = extractTextAndUrls(object.content);
const normalizedUrl = urls.length > 0 ? normalizeUrl(urls[0]) : null; 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({ posts.push({
id: object.id || activity.id, id: object.id || activity.id,
content: text || '', content: contentText || '',
createdAt: object.published || activity.published || new Date().toISOString(), createdAt: object.published || activity.published || new Date().toISOString(),
likesCount: 0, likesCount: 0,
repostsCount: 0, repostsCount: 0,
@@ -220,7 +221,7 @@ export async function GET(request: Request, context: RouteContext) {
altText: sanitizeText(attachment.name) || null, altText: sanitizeText(attachment.name) || null,
})), })),
linkPreviewUrl: linkPreview?.url || normalizedUrl, linkPreviewUrl: linkPreview?.url || normalizedUrl,
linkPreviewTitle: linkPreview?.title || (normalizedUrl ?? null), linkPreviewTitle: linkPreview?.title || null,
linkPreviewDescription: linkPreview?.description || null, linkPreviewDescription: linkPreview?.description || null,
linkPreviewImage: linkPreview?.image || null, linkPreviewImage: linkPreview?.image || null,
}); });
+2 -1
View File
@@ -1357,6 +1357,7 @@ a.btn-primary:visited {
.link-preview-card { .link-preview-card {
display: block; display: block;
margin-top: 12px; margin-top: 12px;
margin-bottom: 12px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
@@ -1543,4 +1544,4 @@ a.btn-primary:visited {
.thread-line { .thread-line {
margin-left: 20px; margin-left: 20px;
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
} }