diff --git a/components/Avatar.vue b/components/Avatar.vue index 49242822..52cc17bb 100644 --- a/components/Avatar.vue +++ b/components/Avatar.vue @@ -3,20 +3,17 @@ diff --git a/routes/profile.vue b/routes/profile.vue index db3da2b8..4bb1dd0d 100644 --- a/routes/profile.vue +++ b/routes/profile.vue @@ -109,6 +109,7 @@ head() { return head({ title: `@${this.username}`, + banner: `banner/@${this.username}.png`, }); }, } diff --git a/server/banner.js b/server/banner.js index fad5e33c..274a5392 100644 --- a/server/banner.js +++ b/server/banner.js @@ -1,37 +1,41 @@ import { buildTemplate, parseTemplates } from "../src/buildTemplate"; import { createCanvas, registerFont, loadImage } from 'canvas'; -import Papa from 'papaparse'; -import fs from 'fs'; +import { loadTsv } from './tsv'; 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) { if (req.url.substr(req.url.length - 4) !== '.png') { - res.statusCode = 404; - res.write('Not found'); - res.end(); - return; + return renderText(res, 'Not found', 404); } const templateName = decodeURIComponent(req.url.substr(1, req.url.length - 5)); - const template = buildTemplate( - parseTemplates(loadTsv('templates/templates.tsv')), - templateName, - ); - const width = 1200 const height = 600 const mime = 'image/png'; 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-700.ttf', { family: 'Quicksand', weight: 'bold'}); @@ -43,22 +47,59 @@ export default async function (req, res, next) { context.fillRect(0, 0, width, height) context.fillStyle = '#000' - const image = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg'); - context.drawImage(image, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25) - - if (template || templateName === 'dowolne') { - 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)) - } else { - context.font = 'regular 120pt Quicksand' - context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + 48) + const fallback = async _ => { + const logo = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg'); + leftRatio = 5; + context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25); + context.font = 'regular 120pt Quicksand'; + context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + 48); } - res.setHeader('content-type', mime); - res.write(canvas.toBuffer(mime)); - res.end() + if (templateName.startsWith('@')) { + const db = await dbConnection(); + 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); } diff --git a/server/nouns.js b/server/nouns.js index 7abeb437..1ccae205 100644 --- a/server/nouns.js +++ b/server/nouns.js @@ -1,3 +1,5 @@ +import {renderJson} from "../src/helpers"; + const dbConnection = require('./db'); const SQL = require('sql-template-strings'); import { ulid } from 'ulid' @@ -60,14 +62,15 @@ export default async function (req, res, next) { const user = authenticate(req); const isAdmin = user && user.authenticated && user.roles === 'admin'; - let result = {error: 'Not found'} if (req.method === 'GET' && req.url === '/all') { - result = await db.all(` + return renderJson(res, await db.all(` SELECT * FROM nouns ${isAdmin ? '' : 'WHERE approved = 1'} 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)) { const id = ulid() await db.get(SQL` @@ -83,19 +86,23 @@ export default async function (req, res, next) { await approve(db, id); } } - result = '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'; + return renderJson(res, 'ok'); } - res.setHeader('content-type', 'application/json'); - res.write(JSON.stringify(result)); - res.end() + if (req.method === 'POST' && req.url.startsWith('/approve/') && isAdmin) { + await approve(db, getId(req.url)); + 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); } diff --git a/server/profile.js b/server/profile.js index 3f007ea5..2d1ca246 100644 --- a/server/profile.js +++ b/server/profile.js @@ -1,6 +1,6 @@ const dbConnection = require('./db'); const SQL = require('sql-template-strings'); -import {buildDict, render} from "../src/helpers"; +import {buildDict, renderJson} from "../src/helpers"; import { ulid } from 'ulid' import authenticate from './authenticate'; import md5 from 'js-md5'; @@ -50,7 +50,7 @@ export default async function (req, res, next) { AND profiles.active = 1 ORDER BY profiles.locale `) - return render(res, buildDict(function* () { + return renderJson(res, buildDict(function* () { for (let profile of profiles) { yield [profile.locale, buildProfile(profile)]; } @@ -58,8 +58,8 @@ export default async function (req, res, next) { } 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); } diff --git a/server/tsv.js b/server/tsv.js new file mode 100644 index 00000000..e3e457bc --- /dev/null +++ b/server/tsv.js @@ -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; +}; diff --git a/server/user.js b/server/user.js index ee94a517..8ce0a795 100644 --- a/server/user.js +++ b/server/user.js @@ -1,5 +1,5 @@ import jwt from './jwt'; -import { makeId } from '../src/helpers'; +import {makeId, renderJson} from '../src/helpers'; const dbConnection = require('./db'); const SQL = require('sql-template-strings'); import { ulid } from 'ulid'; @@ -165,17 +165,17 @@ export default async function (req, res, next) { const db = await dbConnection(); const user = authenticate(req); - let result = {error: 'notfound'} - if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) { - result = 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); + return renderJson(await init(db, req.body.usernameOrEmail)); } - res.setHeader('content-type', 'application/json'); - res.write(JSON.stringify(result)); - res.end(); + if (req.method === 'POST' && req.url === '/validate' && req.body.code) { + return renderJson(await validate(db, user, req.body.code)); + } + + 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); } diff --git a/src/helpers.js b/src/helpers.js index a0352bbe..986abd41 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,3 +1,6 @@ +import md5 from 'js-md5'; +import { Base64 } from 'js-base64'; + export const buildDict = (fn, ...args) => { const dict = {}; for (let [key, value] of fn(...args)) { @@ -83,9 +86,29 @@ export const parseQuery = (queryString) => { return query; } -export const render = (res, content, status = 200) => { - res.status = status; +export const renderText = (res, content, status = 200) => { + res.statusCode = status; res.setHeader('content-type', 'application/json'); res.write(JSON.stringify(content)); 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}`; +}