// 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); } });