PronounsPage/shared/links.ts

98 lines
3.0 KiB
TypeScript

import AbortController from 'abort-controller';
import { JSDOM, VirtualConsole } from 'jsdom';
export const normaliseUrl = (url: string): string | null => {
try {
return new URL(url).toString();
} catch {
return null;
}
};
export class LinkAnalyser {
async analyse(urlString: string) {
const url = new URL(urlString);
let $document;
try {
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000);
// @ts-expect-error: signal does not validate
const htmlString = await (await fetch(url, { signal: controller.signal })).text();
$document = new JSDOM(htmlString, { virtualConsole: new VirtualConsole() });
} catch (e) {
return {
url: url.toString(),
error: e,
};
}
const favicon = await this._findFavicon(url, $document);
return {
url: url.toString(),
relMe: await this._findRelMe($document),
favicon: favicon ? favicon.toString() : null,
nodeinfo: await this._fetchNodeInfo(url),
};
}
async _findRelMe($document: JSDOM) {
const links = new Set();
for (const $el of $document.window.document.querySelectorAll('[rel=me]')) {
if (!['A', 'LINK'].includes($el.tagName)) {
continue;
}
// @ts-expect-error: attributes does not validate
const link = ($el.attributes.value || $el.attributes.href)?.value;
if (!link) {
continue;
}
links.add(link);
}
return [...links];
}
async _findFavicon(url: URL, $document: JSDOM): Promise<URL | null> {
for (const $el of $document.window.document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"], link[rel="mask-icon"]')) {
// @ts-expect-error: attributes does not validate
const link = $el.attributes.href?.value;
if (!link) {
continue;
}
return new URL(link, url);
}
try {
const fallback = new URL('/favicon.ico', url);
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
// @ts-expect-error: signal does not validate
const res = await fetch(fallback, { signal: controller.signal });
if (res.ok) {
return fallback;
}
} catch {}
return null;
}
async _fetchNodeInfo(url: URL) {
try {
const res = await fetch(new URL('/.well-known/nodeinfo', url));
if (res.status !== 200) {
return null;
}
const link = (await res.json())?.links?.[0]?.href;
if (!link) {
return null;
}
return await (await fetch(new URL(link, url))).json();
} catch {
return null;
}
}
}