2026-04-11 17:45:13 -07:00

116 lines
4.0 KiB
TypeScript

import { join } from "jsr:@std/path";
import { serveDir } from "jsr:@std/http/file-server";
const scriptDir = import.meta.dirname ?? ".";
const root = join(scriptDir, "..", "..");
const port = Number(Deno.env.get("PORT") ?? "8080");
function trimPathLine(line: string): string {
let s = line.trim();
if (!s || s.startsWith("#")) return "";
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
s = s.slice(1, -1);
}
return s.trim();
}
/** Optional one-line file in repo root: absolute path to `ChatRequest/Database.json`. */
function readOptionalPathFile(): string | undefined {
try {
const pathFile = join(root, "chat-request-database.path");
const raw = Deno.readTextFileSync(pathFile);
for (const line of raw.split(/\r?\n/)) {
const p = trimPathLine(line);
if (p) return p;
}
} catch {
// missing or unreadable
}
return undefined;
}
const chatRequestDatabase =
Deno.env.get("CHAT_REQUEST_DATABASE")?.trim() || readOptionalPathFile();
function isSafeProxyPath(path: string): boolean {
if (!path) return false;
if (!path.startsWith("/")) return false;
if (path.includes("://")) return false;
if (path.includes("\\") || path.includes("\r") || path.includes("\n")) return false;
return true;
}
async function proxyApiRequest(req: Request, upstreamBase: string): Promise<Response> {
const url = new URL(req.url);
const path = url.searchParams.get("path") ?? "";
if (!isSafeProxyPath(path)) {
return new Response("Invalid path\n", { status: 400 });
}
const upstream = new URL(`${upstreamBase}${path}`);
for (const [key, value] of url.searchParams.entries()) {
if (key === "path") continue;
upstream.searchParams.append(key, value);
}
try {
const upstreamRes = await fetch(upstream, { method: "GET" });
const headers = new Headers();
const contentType = upstreamRes.headers.get("content-type");
if (contentType) headers.set("content-type", contentType);
headers.set("cache-control", "no-store");
return new Response(upstreamRes.body, {
status: upstreamRes.status,
statusText: upstreamRes.statusText,
headers,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return new Response(`Proxy request failed: ${message}\n`, { status: 502 });
}
}
function isChatRequestFilename(pathname: string): boolean {
const base = pathname.split("/").pop() ?? "";
return base === "ChatRequest.json" || base === "database.json";
}
Deno.serve({ port, hostname: "127.0.0.1" }, async (req) => {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/api/beatleader") {
return proxyApiRequest(req, "https://api.beatleader.com");
}
if (req.method === "GET" && url.pathname === "/api/beatsaver") {
return proxyApiRequest(req, "https://api.beatsaver.com");
}
if (req.method === "GET" && chatRequestDatabase && isChatRequestFilename(url.pathname)) {
try {
let text = await Deno.readTextFile(chatRequestDatabase);
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
return new Response(text, {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
console.error(`CHAT_REQUEST_DATABASE not found: ${chatRequestDatabase}`);
return new Response(null, { status: 404 });
}
const msg = e instanceof Error ? e.message : String(e);
console.error(`CHAT_REQUEST_DATABASE read error (${chatRequestDatabase}): ${msg}`);
return new Response(`Failed to read CHAT_REQUEST_DATABASE: ${msg}\n`, { status: 500 });
}
}
return serveDir(req, { fsRoot: root, showDirListing: false });
});
console.log(`Overlay: http://127.0.0.1:${port}/index.html`);
if (chatRequestDatabase) {
console.log(`Chat request database file: ${chatRequestDatabase}`);
} else {
console.warn(
"No database path: set CHAT_REQUEST_DATABASE or create chat-request-database.path (see README). " +
"Otherwise /ChatRequest.json is only served from this folder if the file exists.",
);
}