External link enhancment
This commit is contained in:
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user