This commit is contained in:
parent
f5973ba5a0
commit
52167fb3c9
4 changed files with 690 additions and 494 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@ th_artists.ndjson
|
||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
/__pycache__
|
/__pycache__
|
||||||
|
changelist_ids.json
|
||||||
|
|
|
||||||
226
index.ts
226
index.ts
|
|
@ -1,8 +1,23 @@
|
||||||
import { parseHTML } from "linkedom"
|
import { parseHTML } from "linkedom"
|
||||||
import { ndjsonToJson, tripleBool, tripleBoolToString } from "./lib";
|
import { ndjsonToJson, prettyStringify, requireBasicAuth, tripleBool, type Change, type Entry } from "./lib";
|
||||||
import * as fs from "fs/promises"
|
import * as fs from "fs/promises"
|
||||||
import { existsSync } from "fs";
|
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"
|
const tracker_page = "https://docs.google.com/spreadsheets/u/0/d/1Z8aANbxXbnUGoZPRvJfWL3gz6jrzPPrwVt3d0c1iJ_4/htmlview/sheet?headers=false&gid=1884837542"
|
||||||
|
|
||||||
async function getTH() {
|
async function getTH() {
|
||||||
|
|
@ -46,52 +61,217 @@ async function getTH() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runComparison() {
|
async function runComparison() {
|
||||||
console.log("Comparing..")
|
|
||||||
const data = await getTH();
|
const data = await getTH();
|
||||||
const old = (await fs.readFile("./th_artists.ndjson")).toString("utf8");
|
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 = ndjsonToJson(old);
|
const oldJson: Entry[] = ndjsonToJson(old) as Entry[];
|
||||||
const newJson = ndjsonToJson(data);
|
const newJson: Entry[] = ndjsonToJson(data) as Entry[];
|
||||||
|
|
||||||
const oldMap = Object.fromEntries(oldJson.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 newMap = Object.fromEntries(newJson.map(item => [item.name, item]));
|
||||||
|
|
||||||
let message = "## TH Change Detection\n\n";
|
const changes: Change[] = [];
|
||||||
|
|
||||||
for (const name in oldMap) {
|
for (const name in oldMap) {
|
||||||
const oldItem = oldMap[name];
|
const oldItem = oldMap[name]!;
|
||||||
const newItem = newMap[name];
|
const newItem = newMap[name];
|
||||||
|
|
||||||
if (!newItem) {
|
if (!newItem) {
|
||||||
message += `**DELETED**: \`${name}\`\n`;
|
changes.push({ op: "delete", name });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldItem.url !== newItem.url) message += `**CHANGED URL** for \`${name}\`\n`;
|
const delta: Partial<Entry> = {};
|
||||||
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.url !== newItem.url) delta.url = newItem.url;
|
||||||
if (oldItem.updated !== newItem.updated) message += `**CHANGED UPDATED** for \`${name}\`, from ${tripleBoolToString(oldItem.updated)} to ${tripleBoolToString(newItem.updated)}\n`;
|
if (oldItem.credits !== newItem.credits) delta.credits = newItem.credits;
|
||||||
if (oldItem.best !== newItem.best) message += `**CHANGED BEST STATUS** for \`${name}\`, from ${oldItem.best ? "Yes" : "No"} to ${newItem.best ? "Yes" : "No"}\n`;
|
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) {
|
for (const name in newMap) {
|
||||||
if (!oldMap[name]) {
|
if (!oldMap[name]) {
|
||||||
message += `**NEW**: \`${name}\`\n`;
|
changes.push({ op: "create", name, item: newMap[name]! });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.trim() !== "## Change Detection") {
|
let message = "";
|
||||||
await fetch(process.env.WEBHOOK_URL!, {
|
|
||||||
|
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',
|
method: 'POST',
|
||||||
headers: { "Content-Type": "application/json" },
|
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);
|
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")) {
|
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());
|
await fs.writeFile("./th_artists.ndjson", await getTH());
|
||||||
|
|
@ -102,17 +282,3 @@ if (!existsSync("./th_artists.ndjson")) {
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await runComparison();
|
await runComparison();
|
||||||
}, 50000)
|
}, 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
|
|
||||||
});
|
|
||||||
|
|
|
||||||
31
lib.ts
31
lib.ts
|
|
@ -22,7 +22,12 @@ export enum TripleBool {
|
||||||
YES = 1,
|
YES = 1,
|
||||||
NO = 0
|
NO = 0
|
||||||
}
|
}
|
||||||
|
export interface Entry { name: string, url: string, credits: string, updated: TripleBool, links_work: TripleBool, best: boolean };
|
||||||
|
|
||||||
|
export type Change =
|
||||||
|
| { op: "delete"; name: string }
|
||||||
|
| { op: "create"; name: string; item: Entry }
|
||||||
|
| { op: "update"; name: string; changes: Partial<Entry> };
|
||||||
const TripleBoolStrings: Record<TripleBool, string> = {
|
const TripleBoolStrings: Record<TripleBool, string> = {
|
||||||
[TripleBool.MOSTLY]: "Mostly",
|
[TripleBool.MOSTLY]: "Mostly",
|
||||||
[TripleBool.YES]: "Yes",
|
[TripleBool.YES]: "Yes",
|
||||||
|
|
@ -45,8 +50,32 @@ export function tripleBool(bool: string): TripleBool {
|
||||||
throw new Error("tripleBool conversion function errored, mysteriously! Passed in: " + bool)
|
throw new Error("tripleBool conversion function errored, mysteriously! Passed in: " + bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prettyStringify(z: unknown) {
|
||||||
|
return JSON.stringify(z, null, 2).replace(/\n {2}"/g, ' "').replace(/\n}/, " }")
|
||||||
|
}
|
||||||
|
|
||||||
export function ndjsonToJson(ndjson: string): any[] {
|
export function requireBasicAuth(req: Request): Response | null {
|
||||||
|
const auth = req.headers.get("authorization");
|
||||||
|
if (!auth || !auth.startsWith("Basic ")) {
|
||||||
|
return new Response("Auth required", {
|
||||||
|
status: 401,
|
||||||
|
headers: { "WWW-Authenticate": 'Basic realm="acx-sheets"' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = Buffer.from(auth.slice(6), "base64").toString("utf8");
|
||||||
|
const [user, pass] = decoded.split(":");
|
||||||
|
|
||||||
|
if (
|
||||||
|
user !== process.env.MERGE_USER ||
|
||||||
|
pass !== process.env.MERGE_PASS
|
||||||
|
) {
|
||||||
|
return new Response("Forbidden", { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function ndjsonToJson(ndjson: string): unknown[] {
|
||||||
return ndjson.split("\n").map(z => {
|
return ndjson.split("\n").map(z => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(z)
|
return JSON.parse(z)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue