blaze/index.ts

321 lines
9.1 KiB
TypeScript
Raw Normal View History

2023-06-18 21:20:52 +00:00
import express from "express";
import { Readability, isProbablyReaderable } from "@mozilla/readability";
2023-06-18 21:20:52 +00:00
import got from "got";
2023-06-18 22:44:17 +00:00
import path from "path";
import { fileURLToPath } from "url";
2023-06-19 12:11:22 +00:00
import "dotenv/config";
2023-06-21 00:26:24 +00:00
import { parseHTML, parseJSON } from "linkedom";
2023-06-20 10:19:17 +00:00
// @ts-ignore
import XHR2 from "xhr2";
const XMLHttpRequest = XHR2.XMLHttpRequest;
import { minify } from "html-minifier";
import {
blazeFunctionality,
blazeUrl,
2023-06-23 22:42:39 +00:00
highlightBlazedLinks,
injectBlazeToPageLinks,
} from "./utils.js";
2023-06-23 14:16:35 +00:00
import etag from "etag";
2023-06-22 14:55:35 +00:00
import compression from "compression";
2023-06-23 14:16:35 +00:00
import fs from "fs";
2023-06-22 14:55:35 +00:00
2023-06-18 21:20:52 +00:00
const app = express();
const port = 8888;
2023-06-18 21:20:52 +00:00
const minifierOptions = {
collapseWhitespace: true,
removeComments: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeTagWhitespace: true,
useShortDoctype: true,
minifyCSS: true,
};
2023-06-18 22:46:43 +00:00
// @ts-ignore
2023-06-18 22:44:17 +00:00
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
2023-06-23 14:16:35 +00:00
// Middlewares
2023-06-22 14:55:35 +00:00
app.use(compression());
2023-06-23 14:16:35 +00:00
app.use((req, res, next) => {
res.set("Cache-Control", "public, max-age=60000");
res.set("Service-Worker-Allowed", "/");
next();
});
2023-06-22 14:55:35 +00:00
2023-06-23 14:16:35 +00:00
// Routes
app.get("/", async (req, res) => {
const searchEngine = "https://api.search.brave.com/res/v1/web/search";
2023-06-20 10:19:17 +00:00
const query = req.query.q as string;
2023-06-18 21:20:52 +00:00
if (!query) {
2023-06-23 14:16:35 +00:00
return res.sendFile(path.join(__dirname, "/index.html"));
2023-06-18 21:20:52 +00:00
}
2023-06-20 07:32:07 +00:00
const key = process.env.CYCLIC_BRAVE_KEY;
2023-06-19 12:01:17 +00:00
if (!key) {
throw new Error("No brave key found");
}
2023-06-20 10:19:17 +00:00
try {
const xhr = new XMLHttpRequest();
2023-06-22 14:38:38 +00:00
const formattedQuery = encodeURIComponent(query);
xhr.open(
"GET",
`${searchEngine}?q=${formattedQuery}&safesearch=moderate`,
true
);
2023-06-20 10:19:17 +00:00
xhr.setRequestHeader("Accept", "*/*");
xhr.setRequestHeader("X-Subscription-Token", key);
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status !== 200) {
console.error("XHR request failed:", xhr.status, xhr.statusText);
return;
}
const data = JSON.parse(xhr.responseText);
// @ts-ignore
const results = data.web.results.map(
(result: any) => `
2023-06-20 07:45:18 +00:00
<article>
2023-06-23 22:42:39 +00:00
<h2><a href="${blazeUrl}/blazed?url=${result.url}">
${result.title}
</a></h2>
2023-06-19 13:27:14 +00:00
<span>${result.meta_url.hostname}</span>
<p>${result.description}</p>
2023-06-20 07:45:18 +00:00
</article>
2023-06-19 12:11:22 +00:00
<hr />
2023-06-20 10:19:17 +00:00
`
);
const html = `
<html>
<head>
<meta charset="UTF-8">
2023-06-23 21:02:28 +00:00
<link rel="icon" type="image/x-icon" href="/favicon.svg" />
<link rel="stylesheet" href="/styles/serp.css" media="print" onload="this.media='all'">
2023-06-20 10:19:17 +00:00
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Blaze - ${query}</title>
<style>
body {font-family:sans-serif}
h2 {margin-bottom:0}
span {font-size:.9rem}
</style>
</head>
<body>
<header>
<label>
<a href="/"><strong>BLAZE</strong></a>
<input type="search" value="${query}" />
<button>Blaze it</button>
</label>
</header>
<hr/>
2023-06-20 10:19:17 +00:00
${results.join("")}
<script>
${blazeFunctionality}
2023-06-23 22:42:39 +00:00
${highlightBlazedLinks}
2023-06-23 14:16:35 +00:00
blazeFunctionality('${blazeUrl}')
2023-06-23 22:42:39 +00:00
const links = document.querySelectorAll('a')
highlightBlazedLinks(links)
</script>
2023-06-20 10:19:17 +00:00
</body>
</html>
`;
const minifiedSerp = minify(html, minifierOptions);
2023-06-23 14:16:35 +00:00
res.set("X-Blaze-Etag", etag(minifiedSerp));
res.send(minifiedSerp);
2023-06-20 10:19:17 +00:00
};
xhr.send();
} catch (err) {
console.error(err);
}
2023-06-18 21:20:52 +00:00
});
2023-06-18 22:52:20 +00:00
2023-06-20 09:48:11 +00:00
app.get("/blazed", async (req, res) => {
2023-06-20 07:32:07 +00:00
const pageToBlaze = req.query.url as string;
2023-06-20 09:48:11 +00:00
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", pageToBlaze, true);
xhr.setRequestHeader("Accept", "text/html");
xhr.onreadystatechange = async () => {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status === 404) {
2023-06-23 14:16:35 +00:00
res.sendFile(path.join(__dirname, "/404.html"));
return;
}
if (xhr.status !== 200) {
console.error("XHR request failed:", xhr.status, xhr.statusText);
res.send(
minify(
`
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Blaze - error</title>
</head>
<body>
<div style="text-align: center; display: flex; align-items: center; flex-direction: column; justify-content: center; font-family: sans-serif; width: 100%; height: 100%">
2023-06-22 14:38:38 +00:00
<h1>Blaze could not load the page :(</h1>
<p>Reason: <code>${xhr.status} ${xhr.statusText}</code></p>
<br />
<br />
<p>
If you want (it would be great!) you can report this problem, writing the requested URL and the reason,
at <a href="mailto:support.blaze@dannyspina.com">support.blaze@dannyspina.com</a>
</p>
<br />
<a href="#" role="button" onclick="history.back()">Go back</a>
</div>
</body>
</html>
`,
minifierOptions
)
);
return;
}
const response = xhr.responseText;
const { document } = parseHTML(response);
if (!isProbablyReaderable(document)) {
// TODO: still a lot of bugs, must be refined to handle some cases, like
// cookie banners, etc.
document.querySelectorAll("link").forEach((l) => {
l.remove();
});
document.querySelectorAll("style").forEach((s) => {
2023-06-22 14:38:38 +00:00
s.remove();
});
document.querySelectorAll("script").forEach((s) => {
s.remove();
});
2023-06-22 14:38:38 +00:00
document.querySelectorAll("img").forEach((i) => {
i.remove();
});
document.querySelectorAll("iframe").forEach((f) => {
f.remove();
});
const blazeDisclaimer = document.createElement("div");
blazeDisclaimer.style.width = "100dvw";
blazeDisclaimer.style.border = "1px solid red";
blazeDisclaimer.style.padding = "1rem";
blazeDisclaimer.style.textAlign = "center";
blazeDisclaimer.innerHTML = `
<h2>BLAZE INFO</h2>
<p>
The page you are seeing <strong>could not be correctly blazed</strong> due to these webpage characteristics.
<strong>Blaze served anyway</strong> a lightweight version of the page.
Keep in mind that this kind of pages <strong>can be hard or even impossible to use, read or understand</strong>.
</p>
`;
const referenceElement = document.body.firstChild;
document.body.insertBefore(blazeDisclaimer, referenceElement);
const blazedPage = minify(document.toString(), minifierOptions);
return res.send(blazedPage);
}
//TODO: find if there are more performant ways to remove images or evaluate if is the case to remove images
document.querySelectorAll("img").forEach((img) => img.remove());
const reader = new Readability(document);
const article = reader.parse();
if (!article) {
return res.send("Something went wrong");
}
const blazedPage = `<html><head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>body {font-family: sans-serif}</style>
</head>
<body>
${article.content}
<script>
${injectBlazeToPageLinks}
const url = "${blazeUrl}"
const currentUrl = "${req.query.url}"
injectBlazeToPageLinks(url, currentUrl)
2023-06-23 22:42:39 +00:00
${highlightBlazedLinks}
const links = document.querySelectorAll('a')
highlightBlazedLinks(links)
</script>
</body></html>
`;
const minifiedBlazedPage = minify(blazedPage, minifierOptions);
res.send(minifiedBlazedPage);
};
xhr.send();
2023-06-20 09:48:11 +00:00
} catch (err) {
console.log(err);
}
2023-06-20 07:32:07 +00:00
});
2023-06-19 12:55:28 +00:00
app.get("/info", (_, res) => {
2023-06-23 14:16:35 +00:00
let Etag;
fs.readFile(path.join(__dirname + "/info.html"), "utf8", (err, data) => {
if (err) {
console.error(err);
return;
}
Etag = etag(data);
res.set("X-Blaze-Etag", Etag);
res.sendFile(path.join(__dirname + "/info.html"));
});
2023-06-20 07:32:07 +00:00
});
app.get("/ooops", (_, res) => {
2023-06-23 14:16:35 +00:00
res.sendFile(path.join(__dirname + "/info_not_blazed.html"));
2023-06-20 07:32:07 +00:00
});
2023-06-19 12:55:28 +00:00
2023-06-20 07:41:34 +00:00
app.get("/favicon.svg", (_, res) => {
res.sendFile(path.join(__dirname + "/favicon.svg"));
});
2023-06-23 21:02:28 +00:00
app.get("/service-worker.js", (_, res) => {
res.sendFile(path.join(__dirname + "/service-worker.js"));
});
app.get("/styles/serp.css", (_, res) => {
res.sendFile(path.join(__dirname + "/styles/serp.css"));
});
2023-06-18 22:52:20 +00:00
app.listen(port, () => {
console.log(`Got request`);
});