link-preview-js
Version:
Javascript module to extract and fetch HTTP link information from blocks of text.
94 lines (93 loc) • 5.2 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPreviewFromContent = void 0;
exports.getLinkPreview = getLinkPreview;
const constants_1 = require("./constants");
const shared_1 = require("./shared");
var shared_2 = require("./shared");
Object.defineProperty(exports, "getPreviewFromContent", { enumerable: true, get: function () { return shared_2.getPreviewFromContent; } });
/**
* Parses the text, extracts the first link it finds and does a HTTP request
* to fetch the website content, afterwards it tries to parse the internal HTML
* and extract the information via meta tags.
*
* This is the mobile/browser-safe entrypoint: no Node-only dependencies, and no
* `resolveDNSHost` SSRF protection (see ILinkPreviewOptions above for why).
* @param text string, text to be parsed
* @param options ILinkPreviewOptions
*/
function getLinkPreview(text, options) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
if (!text || typeof text !== `string`) {
throw new Error(`link-preview-js did not receive a valid url or text`);
}
const detectedUrl = text
.replace(/\n/g, ` `)
.split(` `)
.find((token) => constants_1.CONSTANTS.REGEX_VALID_URL.test(token));
if (!detectedUrl) {
throw new Error(`link-preview-js did not receive a valid a url or text`);
}
if ((options === null || options === void 0 ? void 0 : options.followRedirects) === `manual` && !(options === null || options === void 0 ? void 0 : options.handleRedirects)) {
throw new Error(`link-preview-js followRedirects is set to manual, but no handleRedirects function was provided`);
}
const timeout = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : 3000; // 3 second timeout default
const controller = new AbortController();
const timeoutCounter = setTimeout(() => controller.abort(), timeout);
const fetchOptions = {
headers: (_b = options === null || options === void 0 ? void 0 : options.headers) !== null && _b !== void 0 ? _b : {},
redirect: (_c = options === null || options === void 0 ? void 0 : options.followRedirects) !== null && _c !== void 0 ? _c : `error`,
signal: controller.signal,
};
const fetchUrl = (options === null || options === void 0 ? void 0 : options.proxyUrl) ? options.proxyUrl.concat(detectedUrl) : detectedUrl;
try {
const fetchWithTimeout = (url) => __awaiter(this, void 0, void 0, function* () {
return fetch(url, fetchOptions).catch((e) => {
if (e.name === `AbortError`) {
throw new Error(`Request timeout`);
}
clearTimeout(timeoutCounter);
throw e;
});
});
let response = yield fetchWithTimeout(fetchUrl);
if (response.status > 300 &&
response.status < 309 &&
fetchOptions.redirect === `manual` &&
(options === null || options === void 0 ? void 0 : options.handleRedirects)) {
const locationHeader = response.headers.get(`location`) || ``;
const isAbsoluteURI = locationHeader.startsWith("http://") || locationHeader.startsWith("https://");
// Resolve the URL, handling both absolute and relative URLs
const forwardedUrl = isAbsoluteURI ? locationHeader : new URL(locationHeader, fetchUrl).href;
if (!options.handleRedirects(fetchUrl, forwardedUrl)) {
throw new Error(`link-preview-js could not handle redirect`);
}
response = yield fetchWithTimeout(forwardedUrl);
}
clearTimeout(timeoutCounter);
const headers = {};
response.headers.forEach((header, key) => {
headers[key] = header;
});
const normalizedResponse = {
url: (options === null || options === void 0 ? void 0 : options.proxyUrl) ? response.url.replace(options.proxyUrl, ``) : response.url,
headers,
data: yield response.text(),
};
return (0, shared_1.parseResponse)(normalizedResponse, options);
}
finally {
clearTimeout(timeoutCounter);
}
});
}