import express from "express";
import * as cheerio from "cheerio";
const app = express();
const PORT = 3000;
const FXP_BASE = "https://www.fxp.co.il";
const fxpHeaders = {
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
};
app.use(express.static("public"));
app.get("/fxp/:threadId", async (req, res) => {
try {
const { threadId } = req.params;
if (!/^\d+$/.test(threadId)) {
return res
.status(400)
.send("Invalid thread ID");
}
const fxpUrl =
`${FXP_BASE}/showthread.php?t=${threadId}`;
console.log("Fetching:", fxpUrl);
const response = await fetch(fxpUrl);
console.log(
"FXP status:",
response.status
);
if (!response.ok) {
const errorBody =
await response.text();
console.log(
"FXP response:",
errorBody.slice(0, 500)
);
return res
.status(response.status)
.send(`
<h1>FXP returned HTTP ${response.status}</h1>
<pre>${escapeHtml(
errorBody.slice(0, 2000)
)}</pre>
`);
}
const html =
await response.text();
const $ =
cheerio.load(html);
$(
[
"script",
"noscript",
"iframe",
"object",
"embed",
"link[rel='preload']",
"link[rel='prefetch']",
"meta[http-equiv='refresh']"
].join(",")
).remove();
/*
* Remove site navigation.
*/
$(
[
"#header",
"#footer",
".header",
".footer",
".navbar",
".navigation",
".breadcrumb",
".sidebar",
".sideBar",
".right-sidebar",
".left-sidebar",
".advertisement",
".advert",
".ads",
".ad",
".banner",
".google-ad",
".share",
".social",
".popup",
".modal"
].join(",")
).remove();
let thread;
if ($("#posts").length) {
thread = $("#posts");
} else if ($(".postlist").length) {
thread = $(".postlist");
} else if ($(".post").length) {
thread = $(".post").first().parent();
} else if ($(".postbit").length) {
thread = $(".postbit").first().parent();
} else if ($(".threadbit").length) {
thread = $(".threadbit").first();
} else {
thread = $("body");
}
thread.find(
[
".signature",
".postbit_controls",
".post_controls",
".postmenu",
".editpost",
".quickreply",
".pagination",
".thread_tools",
".posthead",
".avatar"
].join(",")
).remove();
thread.find(
[
".ad",
".ads",
".advert",
".advertisement",
".share",
".social"
].join(",")
).remove();
const content =
thread.length
? thread.html()
: "<p>Could not find thread content.</p>";
$("body").empty();
$("body").append(`
<main class="fxp-page">
<div class="fxp-thread">
${content}
</div>
</main>
`);
$("link[rel='stylesheet']").remove();
/*
* Add our own CSS.
*/
$("head").append(``);
/*
* Fix image URLs.
*/
$("img").each((_, el) => {
const src =
$(el).attr("src");
if (!src) {
return;
}
try {
$(el).attr(
"src",
new URL(
src,
fxpUrl
).href
);
} catch {}
});
/*
* Fix links.
*/
$("a").each((_, el) => {
const href =
$(el).attr("href");
if (!href) {
return;
}
if (
href.startsWith("#") ||
href.startsWith("javascript:")
) {
return;
}
try {
$(el).attr(
"href",
new URL(
href,
fxpUrl
).href
);
} catch {}
});
/*
* Send HTML.
*/
res.setHeader(
"Content-Type",
"text/html; charset=utf-8"
);
res.send(
$.html()
);
} catch (error) {
console.error(
"FXP proxy error:",
error
);
res.status(500).send(`
<!DOCTYPE html>
<html
lang="he"
dir="rtl"
>
<head>
<meta
charset="UTF-8"
>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<title>
FXP Proxy Error
</title>
</head>
<body>
<h1>
שגיאה
</h1>
<p>
לא ניתן לטעון את האשכול.
</p>
<pre>${escapeHtml(
error?.stack ||
error?.message ||
String(error)
)}</pre>
</body>
</html>
`);
}
});
/*
* Escape HTML for error messages.
*/
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
app.listen(
PORT,
() => {
console.log(
`Server running on http://localhost:${PORT}`
);
}
);