110 lines
2.9 KiB
TypeScript
110 lines
2.9 KiB
TypeScript
|
import { contentType } from "https://deno.land/std@0.202.0/media_types/mod.ts";
|
||
|
import {
|
||
|
Image,
|
||
|
TextLayout,
|
||
|
} from "https://deno.land/x/imagescript@1.2.15/ImageScript.js";
|
||
|
|
||
|
const font = await fetch("https://static.pyrope.net/courier-std-bold.otf")
|
||
|
.then((r) => r.arrayBuffer())
|
||
|
.then((b) => new Uint8Array(b));
|
||
|
|
||
|
// deciding now: pronouns (e.g. he/him) can be at most 32 chars
|
||
|
|
||
|
let images: Map<string, { image: Uint8Array; fetched: number }> = new Map();
|
||
|
|
||
|
function make_image(s: string): Image {
|
||
|
console.log(`MAKING AN IMAGE FOR '${s}' (EXPENSIVE!)`);
|
||
|
let image = Image.renderText(
|
||
|
font,
|
||
|
64,
|
||
|
s,
|
||
|
0,
|
||
|
new TextLayout({ verticalAlign: "center", horizontalAlign: "middle" }),
|
||
|
);
|
||
|
|
||
|
const outline = Image.renderText(
|
||
|
font,
|
||
|
64,
|
||
|
s,
|
||
|
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, vshift);
|
||
|
}
|
||
|
}
|
||
|
const text = Image.renderText(
|
||
|
font,
|
||
|
64,
|
||
|
s,
|
||
|
0x00_00_00_ff,
|
||
|
new TextLayout({ verticalAlign: "center", horizontalAlign: "middle" }),
|
||
|
);
|
||
|
|
||
|
const final = image.composite(text);
|
||
|
return final.crop(0, 0, final.width, final.height - 22);
|
||
|
}
|
||
|
|
||
|
async function get_pronoun_image(prn: string): Promise<Uint8Array> {
|
||
|
if (images.has(prn)) {
|
||
|
const entry = images.get(prn)!;
|
||
|
entry.fetched++;
|
||
|
return entry.image;
|
||
|
}
|
||
|
const image = await make_image(prn).encode();
|
||
|
images.set(prn, { image, fetched: 1 });
|
||
|
return image;
|
||
|
}
|
||
|
|
||
|
// the whole cache thing def sketches me out, but people probably won't be *that* malicious
|
||
|
const MAX_PRN_LENGTH = 32;
|
||
|
const MAX_PRN_CACHE = 64;
|
||
|
// shouldn't really be a problem
|
||
|
const MAX_PRN_CHOICES = 128;
|
||
|
|
||
|
function clean_up() {
|
||
|
if (images.size <= MAX_PRN_CACHE) {
|
||
|
console.log(
|
||
|
`not cleaning up, we only have ${images.size} cached which is less than ${MAX_PRN_CACHE}`,
|
||
|
);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// reverse order!
|
||
|
const entries = [...images.entries()].toSorted(([_a, a], [_b, b]) =>
|
||
|
b.fetched - a.fetched
|
||
|
);
|
||
|
|
||
|
console.log("before clean up:", images);
|
||
|
images = new Map(entries.slice(0, MAX_PRN_CACHE));
|
||
|
console.log("after clean up:", images);
|
||
|
}
|
||
|
|
||
|
const image_response = (data: Uint8Array): Response =>
|
||
|
new Response(data, {
|
||
|
headers: { "Content-Type": contentType("png") },
|
||
|
});
|
||
|
|
||
|
Deno.serve({ port: 61265 }, async (req) => {
|
||
|
const prns = (new URL(req.url)).searchParams.getAll("p");
|
||
|
|
||
|
if (prns.some((p) => p.length > MAX_PRN_LENGTH)) {
|
||
|
return new Response(`MAX_PRN_LENGTH = ${MAX_PRN_LENGTH}`, { status: 413 });
|
||
|
}
|
||
|
|
||
|
if (prns.length > MAX_PRN_CHOICES) {
|
||
|
return new Response(`MAX_PRN_CHOICES = ${MAX_PRN_CHOICES}`, {
|
||
|
status: 413,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const prn = prns[Math.floor(Math.random() * prns.length)] ??
|
||
|
"NONE, APPARENTLY";
|
||
|
|
||
|
const resp = image_response(await get_pronoun_image(prn));
|
||
|
clean_up();
|
||
|
return resp;
|
||
|
});
|