2024-06-26 13:59:53 +02:00

150 lines
6.2 KiB
TypeScript

import { Router } from 'express';
import SQL from 'sql-template-strings';
import { createCanvas, loadImage } from 'canvas';
import type { CanvasRenderingContext2D, Image } from 'canvas';
import { loadSuml, loadSumlFromBase } from '../loader.ts';
import avatar from '../avatar.ts';
import { buildPronoun, parsePronounGroups, parsePronouns } from '../../src/buildPronoun.ts';
import { PronounLibrary } from '../../src/classes.ts';
import { loadTsv } from '../../src/tsv.ts';
import { handleErrorAsync } from '../../src/helpers.ts';
import { Translator } from '../../src/translator.ts';
import { CacheObject } from '../../src/cache.ts';
import { registerLocaleFont } from '../localeFont.ts';
import type { Translations } from '../../locale/translations.ts';
import type { User } from '../../src/user.ts';
const translations = loadSuml('translations') as Translations;
const baseTranslations = loadSumlFromBase('locale/_base/translations') as Translations;
const translator = new Translator(translations, baseTranslations, global.config);
const pronouns = parsePronouns(global.config, loadTsv(`${__dirname}/../../data/pronouns/pronouns.tsv`));
const pronounGroups = parsePronounGroups(loadTsv(`${__dirname}/../../data/pronouns/pronounGroups.tsv`));
const pronounLibrary = new PronounLibrary(global.config, pronounGroups, pronouns);
const drawCircle = (context: CanvasRenderingContext2D, image: Image, x: number, y: number, size: number): void => {
context.save();
context.beginPath();
context.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2, true);
context.closePath();
context.clip();
context.drawImage(image, x, y, size, size);
context.beginPath();
context.arc(x, y, size / 2, 0, Math.PI * 2, true);
context.clip();
context.closePath();
context.restore();
};
// TODO combine logic with mainPronoun.ts
const getPronounNameOptions = (pronounName: string): string[] | undefined => {
if (global.config.pronouns.any && pronounName === global.config.pronouns.any) {
return [translator.translate('pronouns.any.short')];
}
const prefix = `${global.config.pronouns.any}:`;
if (global.config.pronouns.any && pronounName.startsWith(prefix)) {
const merged = pronounLibrary.byKey()[pronounName.substring(prefix.length)];
if (merged) {
return [merged.short(translator)];
}
}
if (global.config.pronouns.null && global.config.pronouns.null.routes?.includes(pronounName)) {
return [pronounName];
}
if (global.config.pronouns.mirror && pronounName === global.config.pronouns.mirror.route) {
return [global.config.pronouns.mirror.name];
}
return buildPronoun(
pronouns,
pronounName,
global.config,
translator,
)?.nameOptions();
};
const router = Router();
router.get('/banner/:pronounName*.png', handleErrorAsync(async (req, res) => {
const width = 1200;
const height = 600;
const mime = 'image/png';
const imageSize = 200;
let leftRatio = 4;
const pronounName = (req.params.pronounName + req.params[0]).replace(/(&amp)+$/, '');
const result = await new CacheObject('banner', `${pronounName}.png`, 24 * 60).fetch(async () => {
const fontName = registerLocaleFont('fontHeadings', ['regular', 'bold']);
const canvas = createCanvas(width, height);
const context = canvas.getContext('2d');
const bg = await loadImage('static/bg.png');
context.drawImage(bg, 0, 0, width, height);
const logo = await loadImage('static/logo/logo.svg');
const logoPrimary = await loadImage('static/logo/logo-primary.svg');
const fallback = async () => {
leftRatio = 5;
context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize, imageSize);
context.font = `regular ${translations.title.length < 10 ? 120 : translations.title.length < 14 ? 80 : 72}pt '${fontName}'`;
context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + (translations.title.length < 10 ? 48 : translations.title.length < 14 ? 36 : 24));
};
if (pronounName.startsWith('@')) {
const user = await req.db.get<Pick<User, 'id' | 'username' | 'email' | 'avatarSource'>>(SQL`SELECT id, username, email, avatarSource FROM users WHERE username=${pronounName.substring(1)}`);
if (!user) {
await fallback();
return canvas.toBuffer(mime);
}
try {
const avatarImage = await loadImage(await avatar(req.db, user));
drawCircle(context, avatarImage, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize);
} catch {}
context.font = `regular 48pt '${fontName}'`;
context.fillText(`@${user.username}`, width / leftRatio + imageSize, height / 2);
context.font = `regular 24pt '${fontName}'`;
context.fillStyle = '#C71585';
const logoSize = 24 * 1.25;
context.drawImage(logoPrimary, width / leftRatio + imageSize, height / 2 + logoSize - 8, logoSize, logoSize);
context.fillText(translations.title, width / leftRatio + imageSize + 36, height / 2 + 48);
return canvas.toBuffer(mime);
}
const pronounNameOptions = getPronounNameOptions(pronounName);
if (pronounName === 'zaimki' || pronounNameOptions === undefined) {
await fallback();
return canvas.toBuffer(mime);
}
context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize, imageSize);
context.font = `regular 48pt '${fontName}'`;
context.fillText(`${translations.pronouns.intro}:`, width / leftRatio + imageSize / 1.5, height / 2 - 36);
context.font = `bold ${pronounNameOptions.length <= 2 ? '70' : '36'}pt '${fontName}'`;
context.fillText(
pronounNameOptions.map((o) => o.replace(/ ?\[[^\]]+] ?/g, '').trim()).join('\n'),
width / leftRatio + imageSize / 1.5,
height / 2 + (pronounNameOptions.length <= 2 ? 72 : 24),
);
return canvas.toBuffer(mime);
});
return res.set('content-type', mime).send(result);
}));
export default router;