ditto/packages/utils/unfurl.ts
2025-02-17 15:32:18 -06:00

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 };