mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import { DittoConf } from '@ditto/conf';
|
|
import { cachedLinkPreviewSizeGauge } from '@ditto/metrics';
|
|
import TTLCache from '@isaacs/ttlcache';
|
|
import { logi } from '@soapbox/logi';
|
|
import { safeFetch } from '@soapbox/safe-fetch';
|
|
import DOMPurify from 'isomorphic-dompurify';
|
|
import { unfurl } from 'unfurl.js';
|
|
|
|
import { PreviewCard } from '@/entities/PreviewCard.ts';
|
|
import { errorJson } from './log.ts';
|
|
|
|
async function unfurlCard(conf: DittoConf, url: string, signal: AbortSignal): Promise<PreviewCard | null> {
|
|
try {
|
|
const result = await unfurl(url, {
|
|
fetch: (url) =>
|
|
safeFetch(url, {
|
|
headers: {
|
|
'Accept': 'text/html, application/xhtml+xml',
|
|
'User-Agent': conf.fetchUserAgent,
|
|
},
|
|
signal,
|
|
}),
|
|
});
|
|
|
|
const { oEmbed, title, description, canonical_url, open_graph } = result;
|
|
|
|
const card = {
|
|
type: oEmbed?.type || 'link',
|
|
url: canonical_url || url,
|
|
title: oEmbed?.title || title || '',
|
|
description: open_graph?.description || description || '',
|
|
author_name: oEmbed?.author_name || '',
|
|
author_url: oEmbed?.author_url || '',
|
|
provider_name: oEmbed?.provider_name || '',
|
|
provider_url: oEmbed?.provider_url || '',
|
|
// @ts-expect-error `html` does in fact exist on oEmbed.
|
|
html: DOMPurify.sanitize(oEmbed?.html || '', {
|
|
ALLOWED_TAGS: ['iframe'],
|
|
ALLOWED_ATTR: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
|
}),
|
|
width: ((oEmbed && oEmbed.type !== 'link') ? oEmbed.width : 0) || 0,
|
|
height: ((oEmbed && oEmbed.type !== 'link') ? oEmbed.height : 0) || 0,
|
|
image: oEmbed?.thumbnails?.[0].url || open_graph?.images?.[0].url || null,
|
|
embed_url: '',
|
|
blurhash: null,
|
|
};
|
|
|
|
logi({ level: 'info', ns: 'ditto.unfurl', url, success: true });
|
|
|
|
return card;
|
|
} catch (e) {
|
|
logi({ level: 'info', ns: 'ditto.unfurl', url, success: false, error: errorJson(e) });
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** TTL cache for preview cards. */
|
|
let previewCardCache: TTLCache<string, Promise<PreviewCard | null>> | undefined;
|
|
|
|
/** Unfurl card from cache if available, otherwise fetch it. */
|
|
function unfurlCardCached(
|
|
conf: DittoConf,
|
|
url: string,
|
|
signal = AbortSignal.timeout(1000),
|
|
): Promise<PreviewCard | null> {
|
|
if (!previewCardCache) {
|
|
previewCardCache = new TTLCache<string, Promise<PreviewCard | null>>(conf.caches.linkPreview);
|
|
}
|
|
const cached = previewCardCache.get(url);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
} else {
|
|
const card = unfurlCard(conf, url, signal);
|
|
previewCardCache.set(url, card);
|
|
cachedLinkPreviewSizeGauge.set(previewCardCache.size);
|
|
return card;
|
|
}
|
|
}
|
|
|
|
export { type PreviewCard, unfurlCardCached };
|