implement merging/denying
Some checks failed
Sheets Deploy Hook / notify (push) Has been cancelled

This commit is contained in:
Soph :3 2025-12-13 12:28:19 +02:00
parent f5973ba5a0
commit 52167fb3c9
4 changed files with 690 additions and 494 deletions

228
index.ts
View file

@ -1,8 +1,23 @@
import {parseHTML} from "linkedom"
import { ndjsonToJson, tripleBool, tripleBoolToString } from "./lib";
import { parseHTML } from "linkedom"
import { ndjsonToJson, prettyStringify, requireBasicAuth, tripleBool, type Change, type Entry } from "./lib";
import * as fs from "fs/promises"
import { existsSync } from "fs";
interface ChangeWithMessage {
changes: Change[],
webhook: unknown
}
let changelist_ids: Record<string, ChangeWithMessage> = {}
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"
async function getTH() {
@ -46,52 +61,217 @@ async function getTH() {
}
async function runComparison() {
console.log("Comparing..")
const data = await getTH();
const old = (await fs.readFile("./th_artists.ndjson")).toString("utf8");
if(Bun.hash(old) !== Bun.hash(data)) {
const oldJson = ndjsonToJson(old);
const newJson = ndjsonToJson(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]));
let message = "## TH Change Detection\n\n";
const changes: Change[] = [];
for (const name in oldMap) {
const oldItem = oldMap[name];
const oldItem = oldMap[name]!;
const newItem = newMap[name];
if (!newItem) {
message += `**DELETED**: \`${name}\`\n`;
changes.push({ op: "delete", name });
continue;
}
if (oldItem.url !== newItem.url) message += `**CHANGED URL** for \`${name}\`\n`;
if (oldItem.credits !== newItem.credits) message += `**CHANGED CREDITS** for \`${name}\`\n`;
if (oldItem.links_work !== newItem.links_work) message += `**CHANGED WORKING LINKS STATUS** for \`${name}\`, from ${tripleBoolToString(oldItem.links_work)} to ${tripleBoolToString(newItem.links_work)}\n`;
if (oldItem.updated !== newItem.updated) message += `**CHANGED UPDATED** for \`${name}\`, from ${tripleBoolToString(oldItem.updated)} to ${tripleBoolToString(newItem.updated)}\n`;
if (oldItem.best !== newItem.best) message += `**CHANGED BEST STATUS** for \`${name}\`, from ${oldItem.best ? "Yes" : "No"} to ${newItem.best ? "Yes" : "No"}\n`;
const delta: Partial<Entry> = {};
if (oldItem.url !== newItem.url) delta.url = newItem.url;
if (oldItem.credits !== newItem.credits) delta.credits = newItem.credits;
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;
if (Object.keys(delta).length > 0) {
changes.push({ op: "update", name, changes: delta });
}
}
for (const name in newMap) {
if (!oldMap[name]) {
message += `**NEW**: \`${name}\`\n`;
changes.push({ op: "create", name, item: newMap[name]! });
}
}
if (message.trim() !== "## Change Detection") {
await fetch(process.env.WEBHOOK_URL!, {
let message = "";
for (const c of changes) {
if (c.op === "delete") {
message += `**DELETED**: \`${c.name}\`\n`;
} else if (c.op === "create") {
message += `**NEW**: \`${c.name}\`\n`;
} else if (c.op === "update") {
for (const k in c.changes) {
message += `**CHANGED ${k.toUpperCase()}** for \`${c.name}\`\n`;
}
}
}
if (message) {
const id = crypto.randomUUID().split("-")[0]!;
const z = await fetch(process.env.WEBHOOK_URL! + "?wait=true&with_components=true", {
method: 'POST',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: message })
body: JSON.stringify({
"components": [
{
"type": 1,
"components": [
{
"type": 2,
"style": 5,
"label": "Accept",
"emoji": {
"name": "✅"
},
"url": `${process.env.ORIGIN}/merge-th/${id}`,
},
{
"type": 2,
"style": 5,
"url": `${process.env.ORIGIN}/ignore-th/${id}`,
"label": "Deny",
"emoji": {
"name": "❌"
}
}
]
}
],
"embeds": [{
"title": "TH Tracker Changes",
"description": message,
}]
})
});
const json = await z.json()
changelist_ids[id] = {
changes,
webhook: json
}
await updateChangelistIds();
}
await fs.writeFile("./th_artists.ndjson", data);
}
}
Bun.serve({
routes: {
"/": () => new Response("Sheets v2"),
"/artists.ndjson": async () => new Response(await fs.readFile("artists.ndjson")),
"/th_artists.ndjson": async () => new Response(await fs.readFile("th_artists.ndjson")),
"/ignore-th/:id": async (req) => {
const authFail = requireBasicAuth(req);
if (authFail) return authFail;
const id = req.params.id;
const changes = changelist_ids[id];
if (!changes) {
return new Response("Id is invalid.", { status: 404 });
}
const embed = changes.webhook.embeds[0];
embed.title = embed.title + " - Denied."
await fetch(process.env.WEBHOOK_URL! + "/messages/" + changes.webhook.id + "?with_components=true", {
method: 'PATCH',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
embeds: [embed],
components: []
})
});
delete changelist_ids[id];
await updateChangelistIds();
return new Response("Ignored successfully.");
},
"/merge-th/:id": async (req) => {
const authFail = requireBasicAuth(req);
if (authFail) return authFail;
const id = req.params.id;
const changes = changelist_ids[id];
if (!changes) {
return new Response("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])
);
for (const change of changes.changes) {
if (change.op === "delete") {
map.delete(change.name);
}
if (change.op === "create") {
map.set(change.name, change.item);
}
if (change.op === "update") {
const item = map.get(change.name);
if (!item) continue;
Object.assign(item, change.changes);
}
}
const merged =
[...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")
);
const embed = changes.webhook.embeds[0];
embed.title = embed.title + " - Accepted!"
await fetch(process.env.WEBHOOK_URL! + "/messages/" + changes.webhook.id + "?with_components=true", {
method: 'PATCH',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
embeds: [embed],
components: []
})
});
delete changelist_ids[id];
await updateChangelistIds();
return new Response("Merged successfully.");
}
},
fetch() {
return new Response("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.")
await fs.writeFile("./th_artists.ndjson", await getTH());
@ -102,17 +282,3 @@ if (!existsSync("./th_artists.ndjson")) {
setInterval(async () => {
await runComparison();
}, 50000)
Bun.serve({
routes: {
"/": () => new Response("Sheets v2"),
"/artists.ndjson": async () => new Response(await fs.readFile("artists.ndjson")),
"/th_artists.ndjson": async () => new Response(await fs.readFile("th_artists.ndjson")),
},
fetch() {
return new Response("Unmatched route");
},
hostname: "0.0.0.0",
port: 5000
});