support multiple origins

This commit is contained in:
Soph :3 2026-01-03 15:27:50 +02:00
parent 40d982de7b
commit 9151402408

278
index.ts
View file

@ -1,42 +1,52 @@
import { parseHTML } from "linkedom"
import { jsonToCsv, ndjsonToJson, prettyStringify, requireBasicAuth, tripleBool, type Change, type Entry } from "./lib";
import * as fs from "fs/promises"
import { parseHTML } from "linkedom";
import {
jsonToCsv,
ndjsonToJson,
prettyStringify,
requireBasicAuth,
tripleBool,
type Change,
type Entry,
} from "./lib";
import * as fs from "fs/promises";
import { existsSync } from "fs";
import { $ } from "bun"
import { $, type BodyInit } from "bun";
interface ChangeWithMessage {
changes: Change[],
webhook: unknown
changes: Change[];
webhook: unknown;
}
let changelist_ids: Record<string, ChangeWithMessage> = {}
let changelist_ids: Record<string, ChangeWithMessage> = {};
if(existsSync("./changelist_ids.json")) {
changelist_ids = JSON.parse((await fs.readFile("./changelist_ids.json")).toString("utf8"))
if (existsSync("./changelist_ids.json")) {
changelist_ids = JSON.parse(
(await fs.readFile("./changelist_ids.json")).toString("utf8"),
);
}
async function updateChangelistIds() {
await fs.writeFile("./changelist_ids.json", JSON.stringify(changelist_ids));
}
const tracker_page = "https://docs.google.com/spreadsheets/u/0/d/1Z8aANbxXbnUGoZPRvJfWL3gz6jrzPPrwVt3d0c1iJ_4/htmlview/sheet?headers=false&gid=1884837542"
const tracker_page =
"https://docs.google.com/spreadsheets/u/0/d/1Z8aANbxXbnUGoZPRvJfWL3gz6jrzPPrwVt3d0c1iJ_4/htmlview/sheet?headers=false&gid=1884837542";
async function getTH() {
const req = await fetch(tracker_page);
const txt = await req.text()
const txt = await req.text();
const { document } = parseHTML(txt);
const table_body = document.querySelector(".waffle > tbody");
if(!table_body) throw new Error("Missing table body..")
if (!table_body) throw new Error("Missing table body..");
//@ts-expect-error .children can be spread-operator'd
const rows = [...table_body.children]
const rows = [...table_body.children];
let ndjson = "";
for (let i = 4; i < rows.length; i++) {
if(!rows[i].children[1]) {
if (!rows[i].children[1]) {
break;
}
let trackerName = rows[i].children[1].innerText;
@ -53,11 +63,19 @@ async function getTH() {
const best = trackerName.startsWith("⭐");
trackerName = trackerName
.replace(/\p{Extended_Pictographic}/gu, '')
.replace(/[\uFE00-\uFE0F]/g, '')
.replace(/\u200D/g, '')
.replace(/\p{Extended_Pictographic}/gu, "")
.replace(/[\uFE00-\uFE0F]/g, "")
.replace(/\u200D/g, "")
.trim();
ndjson += JSON.stringify({ name: trackerName, url: trackerUrl, credit, updated, links_work, best }) + "\n";
ndjson +=
JSON.stringify({
name: trackerName,
url: trackerUrl,
credit,
updated,
links_work,
best,
}) + "\n";
}
return ndjson;
@ -67,12 +85,12 @@ async function runComparison() {
const data = await getTH();
const old = (await fs.readFile("./th_artists.ndjson")).toString("utf8");
if(Bun.hash(old) !== Bun.hash(data)) {
if (Bun.hash(old) !== Bun.hash(data)) {
const oldJson: Entry[] = ndjsonToJson(old) as Entry[];
const newJson: Entry[] = ndjsonToJson(data) as Entry[];
const oldMap = Object.fromEntries(oldJson.map(item => [item.name, item]));
const newMap = Object.fromEntries(newJson.map(item => [item.name, item]));
const oldMap = Object.fromEntries(oldJson.map((item) => [item.name, item]));
const newMap = Object.fromEntries(newJson.map((item) => [item.name, item]));
const changes: Change[] = [];
@ -89,7 +107,8 @@ async function runComparison() {
if (oldItem.url !== newItem.url) delta.url = newItem.url;
if (oldItem.credit !== newItem.credit) delta.credit = newItem.credit;
if (oldItem.links_work !== newItem.links_work) delta.links_work = newItem.links_work;
if (oldItem.links_work !== newItem.links_work)
delta.links_work = newItem.links_work;
if (oldItem.updated !== newItem.updated) delta.updated = newItem.updated;
if (oldItem.best !== newItem.best) delta.best = newItem.best;
@ -121,50 +140,54 @@ async function runComparison() {
if (message) {
const id = crypto.randomUUID().split("-")[0]!;
const z = await fetch(process.env.WEBHOOK_URL! + "?wait=true&with_components=true", {
method: 'POST',
const z = await fetch(
process.env.WEBHOOK_URL! + "?wait=true&with_components=true",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"components": [
components: [
{
"type": 1,
"components": [
type: 1,
components: [
{
"type": 2,
"style": 5,
"label": "Accept",
"emoji": {
"name": "✅"
type: 2,
style: 5,
label: "Accept",
emoji: {
name: "✅",
},
"url": `${process.env.PUBLIC}/merge-th/${id}`,
url: `${process.env.PUBLIC}/merge-th/${id}`,
},
{
"type": 2,
"style": 5,
"url": `${process.env.PUBLIC}/ignore-th/${id}`,
"label": "Deny",
"emoji": {
"name": "❌"
}
}
]
}
type: 2,
style: 5,
url: `${process.env.PUBLIC}/ignore-th/${id}`,
label: "Deny",
emoji: {
name: "❌",
},
},
],
"embeds": [{
"title": "TH Tracker Changes",
"description": message,
}]
})
});
},
],
embeds: [
{
title: "TH Tracker Changes",
description: message,
},
],
}),
},
);
const json = await z.json()
const json = await z.json();
changelist_ids[id] = {
changes,
webhook: json
}
webhook: json,
};
await updateChangelistIds();
}
await fs.writeFile("./th_artists.ndjson", data);
@ -172,41 +195,70 @@ async function runComparison() {
}
export class ClientResponse extends Response {
constructor(body?: BodyInit, init?: ResponseInit) {
constructor(url: URL, body?: BodyInit, init?: ResponseInit) {
super(body, init);
this.headers.set("Access-Control-Allow-Origin", process.env.ORIGIN!);
this.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, DELETE");
const origins = process.env.ORIGIN!.split(",");
if (origins.includes(url.origin)) {
this.headers.set("Access-Control-Allow-Origin", url.origin);
this.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
}
}
static override json(body: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(body), {
static json_c(body: unknown, url: URL, init?: ResponseInit): Response {
const res = new Response(JSON.stringify(body), {
...init,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": process.env.ORIGIN!,
"Access-Control-Allow-Methods": "OPTIONS, GET, POST, PUT, DELETE",
...init?.headers,
},
});
const origins = process.env.ORIGIN!.split(",");
if (origins.includes(url.origin)) {
res.headers.set("Access-Control-Allow-Origin", url.origin);
res.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
return res;
} else {
return res;
}
}
}
Bun.serve({
routes: {
"/": () => new ClientResponse("Sheets v2"),
"/artists.json": async () => ClientResponse.json(
ndjsonToJson((await fs.readFile("artists.ndjson")).toString("utf8"))
"/": (req) => new ClientResponse(new URL(req.url), "Sheets v2"),
"/artists.json": async (req) =>
ClientResponse.json_c(
ndjsonToJson((await fs.readFile("artists.ndjson")).toString("utf8")),
new URL(req.url),
),
"/artists.csv": async () => new ClientResponse(
"/artists.csv": async (req) =>
new ClientResponse(
new URL(req.url),
jsonToCsv(
ndjsonToJson((await fs.readFile("artists.ndjson")).toString("utf8")
)
), {
ndjsonToJson((await fs.readFile("artists.ndjson")).toString("utf8")),
),
{
headers: {
"Content-Type": "text/csv"
}
}),
"/artists.ndjson": async () => new ClientResponse(await fs.readFile("artists.ndjson")),
"/th_artists.ndjson": async () => new ClientResponse(await fs.readFile("th_artists.ndjson")),
"Content-Type": "text/csv",
},
},
),
"/artists.ndjson": async (req) =>
new ClientResponse(new URL(req.url), await fs.readFile("artists.ndjson")),
"/th_artists.ndjson": async (req) =>
new ClientResponse(
new URL(req.url),
await fs.readFile("th_artists.ndjson"),
),
"/ignore-th/:id": async (req) => {
const authFail = requireBasicAuth(req);
if (authFail) return authFail;
@ -215,25 +267,33 @@ Bun.serve({
const changes = changelist_ids[id];
if (!changes) {
return new ClientResponse("Id is invalid.", { status: 404 });
return new ClientResponse(new URL(req.url), "Id is invalid.", {
status: 404,
});
}
const embed = changes.webhook.embeds[0];
embed.title = embed.title + " - Denied."
const embed = (changes.webhook as { embeds: any[] }).embeds[0];
embed.title = embed.title + " - Denied.";
await fetch(process.env.WEBHOOK_URL! + "/messages/" + changes.webhook.id + "?with_components=true", {
method: 'PATCH',
await fetch(
process.env.WEBHOOK_URL! +
"/messages/" +
(changes.webhook as { id: string }).id +
"?with_components=true",
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
embeds: [embed],
components: []
})
});
components: [],
}),
},
);
delete changelist_ids[id];
await updateChangelistIds();
return new ClientResponse("Ignored successfully.");
return new ClientResponse(new URL(req.url), "Ignored successfully.");
},
"/merge-th/:id": async (req) => {
const authFail = requireBasicAuth(req);
@ -243,15 +303,15 @@ Bun.serve({
const changes = changelist_ids[id];
if (!changes) {
return new ClientResponse("Id is invalid.", { status: 404 });
return new ClientResponse(new URL(req.url), "Id is invalid.", {
status: 404,
});
}
const artistsRaw = await fs.readFile("./artists.ndjson", "utf8");
const artists = ndjsonToJson(artistsRaw) as Entry[];
const map = new Map<string, Entry>(
artists.map(a => [a.name, a])
);
const map = new Map<string, Entry>(artists.map((a) => [a.name, a]));
for (const change of changes.changes) {
if (change.op === "delete") {
@ -270,27 +330,31 @@ Bun.serve({
}
const merged =
[...map.values()]
.map(v => prettyStringify(v))
.join("\n") + "\n";
[...map.values()].map((v) => prettyStringify(v)).join("\n") + "\n";
await fs.writeFile("./artists.ndjson", merged);
await fs.writeFile(
"./th_artists.ndjson",
await fs.readFile("./th_artists.ndjson")
await fs.readFile("./th_artists.ndjson"),
);
const embed = changes.webhook.embeds[0];
embed.title = embed.title + " - Accepted!"
const embed = (changes.webhook as { embeds: any[] }).embeds[0];
embed.title = embed.title + " - Accepted!";
await fetch(process.env.WEBHOOK_URL! + "/messages/" + changes.webhook.id + "?with_components=true", {
method: 'PATCH',
await fetch(
process.env.WEBHOOK_URL! +
"/messages/" +
(changes.webhook as { id: string }).id +
"?with_components=true",
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
embeds: [embed],
components: []
})
});
components: [],
}),
},
);
delete changelist_ids[id];
await updateChangelistIds();
@ -315,21 +379,27 @@ Bun.serve({
}
} catch (err) {
console.error("Error:", err);
return new ClientResponse("Failed to automerge to git. Error: " + err, { status: 400 });
return new ClientResponse(
new URL(req.url),
"Failed to automerge to git. Error: " + err,
{
status: 400,
},
);
}
return new ClientResponse("Merged successfully.");
}
return new ClientResponse(new URL(req.url), "Merged successfully.");
},
fetch() {
return new ClientResponse("Unmatched route");
},
fetch(req) {
return new ClientResponse(new URL(req.url), "Unmatched route");
},
hostname: process.env.HOST || "127.0.0.1",
});
if (!existsSync("./th_artists.ndjson")) {
console.log("Assuming first run. Downloading TH info and waiting 50s.")
console.log("Assuming first run. Downloading TH info and waiting 50s.");
await fs.writeFile("./th_artists.ndjson", await getTH());
} else {
await runComparison();
@ -337,4 +407,4 @@ if (!existsSync("./th_artists.ndjson")) {
setInterval(async () => {
await runComparison();
}, 50000)
}, 50000);