forked from BRETT/BRETT
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
const MAX_HTML_BYTES = 512 * 1024
|
|
|
|
function metaContent(html: string, keys: string[]): string | null {
|
|
for (const key of keys) {
|
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
const before = new RegExp(`<meta[^>]+(?:property|name)=["']${escaped}["'][^>]+content=["']([^"']+)["']`, 'i')
|
|
const after = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${escaped}["']`, 'i')
|
|
const match = before.exec(html) ?? after.exec(html)
|
|
if (match?.[1]?.trim()) return match[1].trim()
|
|
}
|
|
return null
|
|
}
|
|
|
|
export interface LinkPreview {
|
|
image: string | null
|
|
title: string | null
|
|
}
|
|
|
|
const BLOCKED_HOST = /^(localhost|127\.|0\.0\.0\.0|::1|\[::1\]|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|.*\.(local|internal|lan)$)/i
|
|
|
|
export function isBlockedHost(hostname: string): boolean {
|
|
return BLOCKED_HOST.test(hostname)
|
|
}
|
|
|
|
export async function fetchLinkPreview(url: string): Promise<LinkPreview> {
|
|
try {
|
|
const hostname = new URL(url).hostname
|
|
if (isBlockedHost(hostname)) return { image: null, title: null }
|
|
const res = await fetch(url, {
|
|
signal: AbortSignal.timeout(5000),
|
|
redirect: 'follow',
|
|
headers: {
|
|
'user-agent': 'Mozilla/5.0 (compatible; 4Brett-LinkPreview/1.0)',
|
|
'accept': 'text/html,application/xhtml+xml',
|
|
},
|
|
})
|
|
if (!res.ok) return { image: null, title: null }
|
|
const contentType = res.headers.get('content-type') ?? ''
|
|
if (!contentType.includes('text/html') && !contentType.includes('application/xhtml')) {
|
|
return { image: null, title: null }
|
|
}
|
|
const html = (await res.text()).slice(0, MAX_HTML_BYTES)
|
|
const base = res.url || url
|
|
|
|
const image = metaContent(html, ['og:image', 'og:image:url', 'og:image:secure_url', 'twitter:image', 'twitter:image:src'])
|
|
const title = metaContent(html, ['og:title', 'twitter:title'])
|
|
?? /<title[^>]*>([^<]+)<\/title>/i.exec(html)?.[1]?.trim()
|
|
?? null
|
|
|
|
return {
|
|
image: image ? new URL(image, base).href.slice(0, 2048) : null,
|
|
title: title?.slice(0, 500) ?? null,
|
|
}
|
|
}
|
|
catch {
|
|
return { image: null, title: null }
|
|
}
|
|
}
|