#50 [card] pronoun cards - banner

This commit is contained in:
Avris 2020-10-16 23:32:51 +02:00
parent 9f252c10c1
commit 08f3f2a649
8 changed files with 156 additions and 77 deletions

View File

@ -3,20 +3,17 @@
</template> </template>
<script> <script>
import md5 from 'js-md5'; import { gravatar } from "../src/helpers";
import { Base64 } from 'js-base64';
export default { export default {
props: { props: {
user: { required: true }, user: { required: true },
size: { 'default': 128 } size: { 'default': 128 }
}, },
computed: { data() {
gravatar(email, size = 128) { return {
const fallback = `https://avi.avris.it/${this.size}/${Base64.encode(this.user.username).replace(/\+/g, '-').replace(/\//g, '_')}.png`; gravatar: gravatar(this.user, this.size),
};
return `https://www.gravatar.com/avatar/${this.user.emailHash || md5(this.user.email)}?d=${encodeURIComponent(fallback)}&s=${this.size}`;
}
}, },
} }
</script> </script>

View File

@ -109,6 +109,7 @@
head() { head() {
return head({ return head({
title: `@${this.username}`, title: `@${this.username}`,
banner: `banner/@${this.username}.png`,
}); });
}, },
} }

View File

@ -1,37 +1,41 @@
import { buildTemplate, parseTemplates } from "../src/buildTemplate"; import { buildTemplate, parseTemplates } from "../src/buildTemplate";
import { createCanvas, registerFont, loadImage } from 'canvas'; import { createCanvas, registerFont, loadImage } from 'canvas';
import Papa from 'papaparse'; import { loadTsv } from './tsv';
import fs from 'fs';
import translations from '../server/translations'; import translations from '../server/translations';
import {gravatar, renderImage, renderText} from "../src/helpers";
const dbConnection = require('./db');
const SQL = require('sql-template-strings');
const drawCircle = (context, image, x, y, size) => {
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();
}
const loadTsv = (filename) => {
return Papa.parse(fs.readFileSync(__dirname + '/../data/' + filename).toString('utf-8'), {
dynamicTyping: true,
header: true,
skipEmptyLines: true,
}).data;
};
export default async function (req, res, next) { export default async function (req, res, next) {
if (req.url.substr(req.url.length - 4) !== '.png') { if (req.url.substr(req.url.length - 4) !== '.png') {
res.statusCode = 404; return renderText(res, 'Not found', 404);
res.write('Not found');
res.end();
return;
} }
const templateName = decodeURIComponent(req.url.substr(1, req.url.length - 5)); const templateName = decodeURIComponent(req.url.substr(1, req.url.length - 5));
const template = buildTemplate(
parseTemplates(loadTsv('templates/templates.tsv')),
templateName,
);
const width = 1200 const width = 1200
const height = 600 const height = 600
const mime = 'image/png'; const mime = 'image/png';
const imageSize = 200; const imageSize = 200;
const leftRatio = template || templateName === 'dowolne' ? 4 : 5; let leftRatio = 4;
registerFont('static/fonts/quicksand-v21-latin-ext_latin-regular.ttf', { family: 'Quicksand', weight: 'regular'}); registerFont('static/fonts/quicksand-v21-latin-ext_latin-regular.ttf', { family: 'Quicksand', weight: 'regular'});
registerFont('static/fonts/quicksand-v21-latin-ext_latin-700.ttf', { family: 'Quicksand', weight: 'bold'}); registerFont('static/fonts/quicksand-v21-latin-ext_latin-700.ttf', { family: 'Quicksand', weight: 'bold'});
@ -43,22 +47,59 @@ export default async function (req, res, next) {
context.fillRect(0, 0, width, height) context.fillRect(0, 0, width, height)
context.fillStyle = '#000' context.fillStyle = '#000'
const image = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg'); const fallback = async _ => {
context.drawImage(image, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25) const logo = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg');
leftRatio = 5;
if (template || templateName === 'dowolne') { context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25);
context.font = 'regular 48pt Quicksand' context.font = 'regular 120pt Quicksand';
context.fillText(translations.template.intro + ':', width / leftRatio + imageSize / 1.5, height / 2 - 36) context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + 48);
const templateNameOptions = templateName === 'dowolne' ? ['dowolne'] : template.nameOptions();
context.font = `bold ${templateNameOptions.length <= 2 ? '70' : '36'}pt Quicksand`
context.fillText(templateNameOptions.join('\n'), width / leftRatio + imageSize / 1.5, height / 2 + (templateNameOptions.length <= 2 ? 72 : 24))
} else {
context.font = 'regular 120pt Quicksand'
context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + 48)
} }
res.setHeader('content-type', mime); if (templateName.startsWith('@')) {
res.write(canvas.toBuffer(mime)); const db = await dbConnection();
res.end() const user = await db.get(SQL`SELECT username, email FROM users WHERE username=${templateName.substring(1)}`);
if (!user) {
await fallback();
return renderImage(res, canvas, mime);
}
const avatar = await loadImage(gravatar(user, imageSize));
drawCircle(context, avatar, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize);
context.font = `regular 48pt Quicksand`
context.fillText('@' + user.username, width / leftRatio + imageSize, height / 2)
const logo = await loadImage('static/favicon.svg');
context.font = 'regular 24pt Quicksand'
context.fillStyle = '#C71585';
const logoSize = 24 * 1.25;
context.drawImage(logo, width / leftRatio + imageSize, height / 2 + logoSize - 4, logoSize, logoSize / 1.25)
context.fillText(translations.title, width / leftRatio + imageSize + 36, height / 2 + 48);
return renderImage(res, canvas, mime);
}
const template = buildTemplate(
parseTemplates(loadTsv(__dirname + '/../data/templates/templates.tsv')),
templateName,
);
const logo = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg');
if (!template && templateName !== 'dowolne') {
await fallback();
return renderImage(res, canvas, mime);
}
context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25)
context.font = 'regular 48pt Quicksand'
context.fillText(translations.template.intro + ':', width / leftRatio + imageSize / 1.5, height / 2 - 36)
const templateNameOptions = templateName === 'dowolne' ? ['dowolne'] : template.nameOptions();
context.font = `bold ${templateNameOptions.length <= 2 ? '70' : '36'}pt Quicksand`
context.fillText(templateNameOptions.join('\n'), width / leftRatio + imageSize / 1.5, height / 2 + (templateNameOptions.length <= 2 ? 72 : 24))
return renderImage(res, canvas, mime);
} }

View File

@ -1,3 +1,5 @@
import {renderJson} from "../src/helpers";
const dbConnection = require('./db'); const dbConnection = require('./db');
const SQL = require('sql-template-strings'); const SQL = require('sql-template-strings');
import { ulid } from 'ulid' import { ulid } from 'ulid'
@ -60,14 +62,15 @@ export default async function (req, res, next) {
const user = authenticate(req); const user = authenticate(req);
const isAdmin = user && user.authenticated && user.roles === 'admin'; const isAdmin = user && user.authenticated && user.roles === 'admin';
let result = {error: 'Not found'}
if (req.method === 'GET' && req.url === '/all') { if (req.method === 'GET' && req.url === '/all') {
result = await db.all(` return renderJson(res, await db.all(`
SELECT * FROM nouns SELECT * FROM nouns
${isAdmin ? '' : 'WHERE approved = 1'} ${isAdmin ? '' : 'WHERE approved = 1'}
ORDER BY approved, masc ORDER BY approved, masc
`); `));
} else if (req.method === 'POST' && req.url === '/submit') { }
if (req.method === 'POST' && req.url === '/submit') {
if (isAdmin || !isTroll(req.body.data)) { if (isAdmin || !isTroll(req.body.data)) {
const id = ulid() const id = ulid()
await db.get(SQL` await db.get(SQL`
@ -83,19 +86,23 @@ export default async function (req, res, next) {
await approve(db, id); await approve(db, id);
} }
} }
result = 'ok'; return renderJson(res, 'ok');
} else if (req.method === 'POST' && req.url.startsWith('/approve/') && isAdmin) {
await approve(db, getId(req.url));
result = 'ok';
} else if (req.method === 'POST' && req.url.startsWith('/hide/') && isAdmin) {
await hide(db, getId(req.url));
result = 'ok';
} else if (req.method === 'POST' && req.url.startsWith('/remove/') && isAdmin) {
await remove(db, getId(req.url));
result = 'ok';
} }
res.setHeader('content-type', 'application/json'); if (req.method === 'POST' && req.url.startsWith('/approve/') && isAdmin) {
res.write(JSON.stringify(result)); await approve(db, getId(req.url));
res.end() return renderJson(res, 'ok');
}
if (req.method === 'POST' && req.url.startsWith('/hide/') && isAdmin) {
await hide(db, getId(req.url));
return renderJson(res, 'ok');
}
if (req.method === 'POST' && req.url.startsWith('/remove/') && isAdmin) {
await remove(db, getId(req.url));
return renderJson(res, 'ok');
}
return renderJson(res, {error: 'Not found'}, 404);
} }

View File

@ -1,6 +1,6 @@
const dbConnection = require('./db'); const dbConnection = require('./db');
const SQL = require('sql-template-strings'); const SQL = require('sql-template-strings');
import {buildDict, render} from "../src/helpers"; import {buildDict, renderJson} from "../src/helpers";
import { ulid } from 'ulid' import { ulid } from 'ulid'
import authenticate from './authenticate'; import authenticate from './authenticate';
import md5 from 'js-md5'; import md5 from 'js-md5';
@ -50,7 +50,7 @@ export default async function (req, res, next) {
AND profiles.active = 1 AND profiles.active = 1
ORDER BY profiles.locale ORDER BY profiles.locale
`) `)
return render(res, buildDict(function* () { return renderJson(res, buildDict(function* () {
for (let profile of profiles) { for (let profile of profiles) {
yield [profile.locale, buildProfile(profile)]; yield [profile.locale, buildProfile(profile)];
} }
@ -58,8 +58,8 @@ export default async function (req, res, next) {
} }
if (!user || !user.authenticated) { if (!user || !user.authenticated) {
return render(res, {error: 'unauthorised'}, 401); return renderJson(res, {error: 'unauthorised'}, 401);
} }
return render(res, { error: 'notfound' }, 404); return renderJson(res, { error: 'notfound' }, 404);
} }

10
server/tsv.js Normal file
View File

@ -0,0 +1,10 @@
import Papa from 'papaparse';
import fs from "fs";
export const loadTsv = (filename) => {
return Papa.parse(fs.readFileSync(filename).toString('utf-8'), {
dynamicTyping: true,
header: true,
skipEmptyLines: true,
}).data;
};

View File

@ -1,5 +1,5 @@
import jwt from './jwt'; import jwt from './jwt';
import { makeId } from '../src/helpers'; import {makeId, renderJson} from '../src/helpers';
const dbConnection = require('./db'); const dbConnection = require('./db');
const SQL = require('sql-template-strings'); const SQL = require('sql-template-strings');
import { ulid } from 'ulid'; import { ulid } from 'ulid';
@ -165,17 +165,17 @@ export default async function (req, res, next) {
const db = await dbConnection(); const db = await dbConnection();
const user = authenticate(req); const user = authenticate(req);
let result = {error: 'notfound'}
if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) { if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
result = await init(db, req.body.usernameOrEmail) return renderJson(await init(db, req.body.usernameOrEmail));
} else if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
result = await validate(db, user, req.body.code);
} else if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
result = await changeUsername(db, user, req.body.username);
} }
res.setHeader('content-type', 'application/json'); if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
res.write(JSON.stringify(result)); return renderJson(await validate(db, user, req.body.code));
res.end(); }
if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
return renderJson(await changeUsername(db, user, req.body.username));
}
return renderJson(res, {error: 'Not found'}, 404);
} }

View File

@ -1,3 +1,6 @@
import md5 from 'js-md5';
import { Base64 } from 'js-base64';
export const buildDict = (fn, ...args) => { export const buildDict = (fn, ...args) => {
const dict = {}; const dict = {};
for (let [key, value] of fn(...args)) { for (let [key, value] of fn(...args)) {
@ -83,9 +86,29 @@ export const parseQuery = (queryString) => {
return query; return query;
} }
export const render = (res, content, status = 200) => { export const renderText = (res, content, status = 200) => {
res.status = status; res.statusCode = status;
res.setHeader('content-type', 'application/json'); res.setHeader('content-type', 'application/json');
res.write(JSON.stringify(content)); res.write(JSON.stringify(content));
res.end(); res.end();
} }
export const renderJson = (res, content, status = 200) => {
res.statusCode = status;
res.setHeader('content-type', 'application/json');
res.write(JSON.stringify(content));
res.end();
}
export const renderImage = (res, canvas, mime, status = 200) => {
res.statusCode = status;
res.setHeader('content-type', mime);
res.write(canvas.toBuffer(mime));
res.end();
}
export const gravatar = (user, size = 128) => {
const fallback = `https://avi.avris.it/${size}/${Base64.encode(user.username).replace(/\+/g, '-').replace(/\//g, '_')}.png`;
return `https://www.gravatar.com/avatar/${user.emailHash || md5(user.email)}?d=${encodeURIComponent(fallback)}&s=${size}`;
}