mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
98 lines
3.0 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|