cohost/serverside/hs2-last-updated.ts

177 lines
5.1 KiB
TypeScript

// once a minute (what's the point if it's out of date!)
// do days since last update (0 will be obvious then), should (please please please let this be true) only need three digits MAX
// until hs2 ends of course but let's not think about that now
// okay aaaa hs2 will end that will be sad and good i guess but
// not important right now
// would be neat (but painful) to have it actually roll like an analog thing
import { contentType } from "https://deno.land/std@0.202.0/media_types/mod.ts";
import {
DOMParser,
NodeList,
NodeType,
} from "https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts";
import {
Image,
TextLayout,
} from "https://deno.land/x/imagescript@1.2.15/ImageScript.js";
// not sure how worthwhile this optimization is but
const dom_parser = new DOMParser();
const murica_short_date = new Intl.DateTimeFormat("en-US", {
month: "numeric",
day: "numeric",
year: "numeric",
});
function get_updates_for_date(
{ date, ps }: { date: Date; ps: NodeList },
): number {
let count = 0;
const looking_for = murica_short_date.format(date) + " - ";
ps.forEach((p) => {
if (
p.childNodes[0] && p.childNodes[0].nodeType == NodeType.TEXT_NODE &&
p.childNodes[0].textContent == looking_for
) count++;
});
return count;
}
async function get_last_update_date(): Promise<
{ last_updated: Date; last_update_count: number } | string
> {
try {
const res = await fetch("https://homestuck2.com/log");
const body = await res.text();
if (!body) return "couldn't get a string body";
// could just regex for the date lol
const doc = dom_parser.parseFromString(body, "text/html");
if (!doc) return "couldn't parse the body into the DOM";
const ps = doc.querySelectorAll("p");
if (!ps) return "couldn't get the ps from the doc";
if (!ps[0]) return "couldn't get even a single p from the doc (bad!)";
// should really enable strict indexing
const us_date_node = ps[0].childNodes[0];
if (!us_date_node || us_date_node.nodeType != NodeType.TEXT_NODE) {
return "couldn't get a date node from the log entry";
}
const us_date = us_date_node.textContent.replace(" - ", "");
// apparently doing new Date("10/8/2023") is implementation defined which is icky but it works with deno
const date = new Date(us_date);
if (Number.isNaN(date.valueOf())) {
return `got an invalid date :(. the text_content was '${us_date}'`;
}
return {
last_updated: date,
last_update_count: get_updates_for_date({ date, ps }),
};
} catch (e) {
return `caught error: ${e}`;
}
}
async function check_again() {
last_checked = new Date();
const stuff = await get_last_update_date();
if (typeof stuff == "string") {
console.log(`failed to get a date when checking 😬: '${stuff}'`);
console.log(
"^ ironically enough, this is more likely when there *is* an update",
);
console.log(
"^ unironically, this is *most* likely the result of a bug",
);
return;
}
last_updated = stuff.last_updated;
last_update_count = stuff.last_update_count;
}
function days_since_update(): number {
const millis = new Date().getTime() - last_updated.getTime();
return millis / (24 * 60 * 60 * 1000);
}
// silly, i know
let last_updated: Date = new Date(0);
let last_checked: Date = new Date();
let last_update_count = 0;
await check_again();
// *definitely* a worthwhile optimization
// (still inefficient to go over http...)
const font = await fetch("https://static.pyrope.net/courier-std-bold.otf")
.then((r) => r.arrayBuffer())
.then((b) => new Uint8Array(b));
function make_image(n: number): Image {
const days = Math.floor(n).toString().padStart(3, "0");
let image = new Image(110, 44);
const outline = Image.renderText(
font,
64,
days,
0xff_ff_ff_ff,
new TextLayout({ verticalAlign: "center", horizontalAlign: "middle" }),
);
for (const hshift of [-2, 0, 2]) {
for (const vshift of [-2, 0, 2]) {
image = image.composite(outline, hshift - 4, vshift);
}
}
const text = Image.renderText(
font,
64,
days,
0x00_00_00_ff,
new TextLayout({ verticalAlign: "center", horizontalAlign: "middle" }),
);
return image.composite(text, -4);
}
async function update_images() {
[last_updated_image, last_update_count_image] = await Promise.all([
make_image(days_since_update()).encode(),
make_image(last_update_count).encode(),
]);
}
let last_updated_image: Uint8Array = new Uint8Array();
let last_update_count_image: Uint8Array = new Uint8Array();
await update_images();
const image_response = (data: Uint8Array): Response =>
new Response(data, {
headers: { "Content-Type": contentType("png") },
});
Deno.serve({ port: 61264 }, async (req) => {
const url = new URL(req.url);
if ((new Date().getTime() - last_checked.getTime()) >= 60 * 1000) {
await check_again();
await update_images();
}
// could use URLPattern but that would be overkill for this
if (url.pathname.match("count")) {
return image_response(last_update_count_image);
} else {
return image_response(last_updated_image);
}
});