135 lines
4.8 KiB
TypeScript
135 lines
4.8 KiB
TypeScript
import { join } from "jsr:@std/path";
|
|
import { serveDir } from "jsr:@std/http/file-server";
|
|
import { parse } from "jsr:@std/toml";
|
|
import type { OverlaySettings } from "../client/types.ts";
|
|
import type { OverlayConfigApiBody } from "../client/overlay-config.ts";
|
|
import { overlayTomlToDefaults, type OverlayToml } from "../client/overlay-config.ts";
|
|
|
|
const scriptDir = import.meta.dirname ?? ".";
|
|
const root = join(scriptDir, "..", "..");
|
|
const port = Number(Deno.env.get("PORT") ?? "8080");
|
|
|
|
function readOverlayConfig(): {
|
|
chatRequestDatabase: string | undefined;
|
|
apiDefaults: Partial<OverlaySettings>;
|
|
} {
|
|
const path = join(root, "overlay.toml");
|
|
let raw: string;
|
|
try {
|
|
raw = Deno.readTextFileSync(path);
|
|
} catch {
|
|
console.warn(
|
|
"overlay.toml not found; using built-in defaults. Copy overlay.toml.example and set at least chat_request_database and beatleader_player_id.",
|
|
);
|
|
return { chatRequestDatabase: undefined, apiDefaults: {} };
|
|
}
|
|
let parsed: OverlayToml;
|
|
try {
|
|
parsed = parse(raw) as OverlayToml;
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
console.error(`overlay.toml: parse error: ${msg}`);
|
|
console.warn(
|
|
"beatleader_player_id is not set; fix overlay.toml or set the BeatLeader id in the URL hash for friend scores.",
|
|
);
|
|
return { chatRequestDatabase: undefined, apiDefaults: {} };
|
|
}
|
|
|
|
const { chatRequestDatabase, defaults, beatleaderPlayerIdConfigured, warnings } = overlayTomlToDefaults(
|
|
parsed,
|
|
);
|
|
for (const w of warnings) console.warn(w);
|
|
if (!beatleaderPlayerIdConfigured) {
|
|
console.warn(
|
|
"beatleader_player_id is not set; friend scores need a BeatLeader id in overlay.toml or in the URL hash.",
|
|
);
|
|
}
|
|
|
|
return { chatRequestDatabase, apiDefaults: defaults };
|
|
}
|
|
|
|
const { chatRequestDatabase, apiDefaults } = readOverlayConfig();
|
|
|
|
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 isChatRequestPath(pathname: string): boolean {
|
|
const base = pathname.split("/").pop() ?? "";
|
|
return base === "ChatRequest.json";
|
|
}
|
|
|
|
Deno.serve({ port, hostname: "127.0.0.1" }, async (req) => {
|
|
const url = new URL(req.url);
|
|
if (req.method === "GET" && url.pathname === "/api/overlay-config") {
|
|
const body: OverlayConfigApiBody = { defaults: apiDefaults };
|
|
return Response.json(body, { headers: { "cache-control": "no-store" } });
|
|
}
|
|
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 && isChatRequestPath(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.warn(
|
|
"No chat_request_database in overlay.toml — place ChatRequest.json in the repo folder or set chat_request_database (see overlay.toml.example).",
|
|
);
|
|
}
|