From 629a12698e4be48bcbebbc065a42a2aac65046c6 Mon Sep 17 00:00:00 2001 From: Avris Date: Tue, 10 Nov 2020 23:41:56 +0100 Subject: [PATCH] #60 [api] Public API --- components/Dictionary.vue | 2 +- components/Footer.vue | 20 ++++++--- components/Login.vue | 4 +- components/NounSubmitForm.vue | 1 - components/Share.vue | 2 +- locale/pl/config.suml | 13 ++++++ locale/pl/translations.suml | 5 +++ nuxt.config.js | 8 +++- plugins/globals.js | 1 + routes/api.vue | 78 +++++++++++++++++++++++++++++++++++ routes/homepage.vue | 4 +- routes/profile.vue | 2 +- routes/profileEditor.vue | 2 +- server/config.js | 3 -- server/index.js | 7 ++-- server/loader.js | 12 ++++++ server/routes/banner.js | 4 +- server/routes/nouns.js | 13 +++++- server/routes/sources.js | 21 +++++++++- server/routes/templates.js | 56 +++++++++++++++++++++++++ server/routes/user.js | 6 ++- server/translations.js | 3 -- src/classes.js | 18 +++----- 23 files changed, 241 insertions(+), 44 deletions(-) create mode 100644 routes/api.vue delete mode 100644 server/config.js create mode 100644 server/loader.js create mode 100644 server/routes/templates.js delete mode 100644 server/translations.js diff --git a/components/Dictionary.vue b/components/Dictionary.vue index 937a3ecf..7773f928 100644 --- a/components/Dictionary.vue +++ b/components/Dictionary.vue @@ -182,7 +182,7 @@ if (this.nounsRaw !== undefined) { return; } - this.nounsRaw = await this.$axios.$get(`/nouns/all`); + this.nounsRaw = await this.$axios.$get(`/nouns`); }, async setFilter(filter) { this.filter = filter; diff --git a/components/Footer.vue b/components/Footer.vue index cbdb3517..d98e1719 100644 --- a/components/Footer.vue +++ b/components/Footer.vue @@ -37,12 +37,20 @@ -

- - - terms.header - -

+ diff --git a/components/Login.vue b/components/Login.vue index 3fa85514..945a91ec 100644 --- a/components/Login.vue +++ b/components/Login.vue @@ -87,8 +87,8 @@ return jwt.verify(this.token, process.env.PUBLIC_KEY, { algorithm: 'RS256', - audience: process.env.BASE_URL, - issuer: process.env.BASE_URL, + audience: this.$base, + issuer: this.$base, }); } }, diff --git a/components/NounSubmitForm.vue b/components/NounSubmitForm.vue index 2b670cee..0a40394b 100644 --- a/components/NounSubmitForm.vue +++ b/components/NounSubmitForm.vue @@ -118,7 +118,6 @@ diff --git a/routes/homepage.vue b/routes/homepage.vue index 20a9d424..eb87c2c4 100644 --- a/routes/homepage.vue +++ b/routes/homepage.vue @@ -243,14 +243,14 @@ if (!this.selectedTemplate.pronoun()) { return null; } - return this.addSlash(process.env.BASE_URL + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink)); + return this.addSlash(this.$base + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink)); }, linkMultiple() { if (!this.multiple.length) { return null; } - return this.addSlash(process.env.BASE_URL + '/' + this.multiple.join('&')); + return this.addSlash(this.$base + '/' + this.multiple.join('&')); }, sources() { return getSources(this.selectedTemplate); diff --git a/routes/profile.vue b/routes/profile.vue index b8f6ba08..cddf34fa 100644 --- a/routes/profile.vue +++ b/routes/profile.vue @@ -141,7 +141,7 @@ for (let pronoun in this.profile.pronouns) { if (!this.profile.pronouns.hasOwnProperty(pronoun)) { continue; } - const link = pronoun.replace(new RegExp('^' + process.env.BASE_URL), '').replace(new RegExp('^/'), ''); + const link = pronoun.replace(new RegExp('^' + this.$base), '').replace(new RegExp('^/'), ''); const template = buildTemplate(templates, link); if (template) { diff --git a/routes/profileEditor.vue b/routes/profileEditor.vue index 8039ca4e..a26c0dea 100644 --- a/routes/profileEditor.vue +++ b/routes/profileEditor.vue @@ -188,7 +188,7 @@ this.$router.push(`/@${this.$user().username}`) }, validatePronoun(pronoun) { - const link = pronoun.replace(new RegExp('^' + process.env.BASE_URL), '').replace(new RegExp('^/'), ''); + const link = pronoun.replace(new RegExp('^' + this.$base), '').replace(new RegExp('^/'), ''); const template = buildTemplate(templates, link); return template ? null : 'profile.pronounsNotFound' diff --git a/server/config.js b/server/config.js deleted file mode 100644 index f90562b6..00000000 --- a/server/config.js +++ /dev/null @@ -1,3 +0,0 @@ -import Suml from 'suml'; -const fs = require('fs'); -export default new Suml().parse(fs.readFileSync('./data/config.suml').toString()); diff --git a/server/index.js b/server/index.js index e53aa389..f0a5ecb8 100644 --- a/server/index.js +++ b/server/index.js @@ -5,7 +5,7 @@ import session from 'express-session'; import cookieParser from 'cookie-parser'; import grant from "grant"; import router from "./routes/user"; -import config from './config'; +import { loadSuml } from './loader'; const app = express() @@ -18,7 +18,7 @@ app.use(session({ })); app.use(async function (req, res, next) { - req.config = config; + req.config = loadSuml('config'); req.rawUser = authenticate(req); req.user = req.rawUser && req.rawUser.authenticated ? req.rawUser : null; req.admin = req.user && req.user.roles === 'admin'; @@ -34,8 +34,9 @@ app.use(require('./routes/user').default); app.use(require('./routes/profile').default); app.use(require('./routes/admin').default); -app.use(require('./routes/nouns').default); +app.use(require('./routes/templates').default); app.use(require('./routes/sources').default); +app.use(require('./routes/nouns').default); export default { path: '/api', diff --git a/server/loader.js b/server/loader.js new file mode 100644 index 00000000..9239adc9 --- /dev/null +++ b/server/loader.js @@ -0,0 +1,12 @@ +const fs = require('fs'); +import Suml from 'suml'; +import Papa from 'papaparse'; + +export const loadSuml = name => new Suml().parse(fs.readFileSync(`./data/${name}.suml`).toString()); + +export const loadTsv = name => Papa.parse(fs.readFileSync(`./data/${name}.tsv`).toString(), { + dynamicTyping: true, + header: true, + skipEmptyLines: true, + delimiter: '\t', +}).data; diff --git a/server/routes/banner.js b/server/routes/banner.js index 13301747..e489e292 100644 --- a/server/routes/banner.js +++ b/server/routes/banner.js @@ -1,11 +1,13 @@ import { Router } from 'express'; import SQL from 'sql-template-strings'; import {createCanvas, loadImage, registerFont} from "canvas"; -import translations from "../translations"; +import { loadSuml } from '../loader'; import avatar from '../avatar'; import {buildTemplate, parseTemplates} from "../../src/buildTemplate"; import {loadTsv} from "../../src/tsv"; +const translations = loadSuml('translations'); + const drawCircle = (context, image, x, y, size) => { context.save(); context.beginPath(); diff --git a/server/routes/nouns.js b/server/routes/nouns.js index d3daabc2..442500e1 100644 --- a/server/routes/nouns.js +++ b/server/routes/nouns.js @@ -23,7 +23,7 @@ const approve = async (db, id) => { const router = Router(); -router.get('/nouns/all', async (req, res) => { +router.get('/nouns', async (req, res) => { return res.json(await req.db.all(SQL` SELECT * FROM nouns WHERE locale = ${req.config.locale} @@ -32,6 +32,17 @@ router.get('/nouns/all', async (req, res) => { `)); }); +router.get('/nouns/search/:term', async (req, res) => { + const term = '%' + req.params.term + '%'; + return res.json(await req.db.all(SQL` + SELECT * FROM nouns + WHERE locale = ${req.config.locale} + AND approved >= ${req.admin ? 0 : 1} + AND (masc like ${term} OR fem like ${term} OR neutr like ${term} OR mascPl like ${term} OR femPl like ${term} OR neutrPl like ${term}) + ORDER BY approved, masc + `)); +}); + router.post('/nouns/submit', async (req, res) => { if (!(req.user && req.user.admin) && isTroll(JSON.stringify(req.body))) { return res.json('ok'); diff --git a/server/routes/sources.js b/server/routes/sources.js index 5c639064..92442c94 100644 --- a/server/routes/sources.js +++ b/server/routes/sources.js @@ -1,6 +1,7 @@ import { Router } from 'express'; import mailer from "../../src/mailer"; -import {camelCase, capitalise} from "../../src/helpers"; +import { camelCase } from "../../src/helpers"; +import { loadTsv } from '../loader'; const generateId = title => { return camelCase(title.split(' ').slice(0, 2)); @@ -21,8 +22,26 @@ const buildEmail = (data, user) => { return `
    ${human.join('')}
${tsv.join('\t')}
`; } +const loadSources = () => { + return loadTsv('sources/sources').map(s => { + if (s.author) { + s.author = s.author.replace('^', ''); + } + s.fragments = s.fragments.split('@').map(f => f.replace(/\|/g, '\n')); + return s; + }); +} + const router = Router(); +router.get('/sources', async (req, res) => { + return res.json(loadSources()); +}); + +router.get('/sources/:key', async (req, res) => { + return res.json([...loadSources().filter(s => s.key === req.params.key), null][0]); +}); + router.post('/sources/submit', async (req, res) => { const emailBody = buildEmail(req.body, req.user); diff --git a/server/routes/templates.js b/server/routes/templates.js new file mode 100644 index 00000000..dcbc9fb2 --- /dev/null +++ b/server/routes/templates.js @@ -0,0 +1,56 @@ +import { Router } from 'express'; +import { loadTsv } from '../loader'; +import {buildTemplate, parseTemplates} from "../../src/buildTemplate"; +import {buildList} from "../../src/helpers"; +import {Example} from "../../src/classes"; + +const buildExample = e => new Example( + Example.parse(e.singular), + Example.parse(e.plural || e.singular), + e.isHonorific, +) + +const requestExamples = r => { + if (!r || !r.length) { + return loadTsv('templates/examples'); + } + + return buildList(function* () { + for (let rr of r) { + let [singular, plural, isHonorific] = rr.split('|'); + yield { singular, plural, isHonorific: !!isHonorific}; + } + }); +} + +const addExamples = (pronoun, examples) => { + return buildList(function* () { + for (let example of examples) { + yield buildExample(example).format(pronoun); + } + }); +} + +const router = Router(); + +router.get('/pronouns', async (req, res) => { + const templates = parseTemplates(loadTsv('templates/templates')); + for (let template in templates) { + if (!templates.hasOwnProperty(template)) { continue; } + templates[template].examples = addExamples(templates[template], requestExamples(req.query.examples)) + } + return res.json(templates); +}); + +router.get('/pronouns/:pronoun*', async (req, res) => { + const pronoun = buildTemplate( + parseTemplates(loadTsv('templates/templates')), + req.params.pronoun + req.params[0], + ); + if (pronoun) { + pronoun.examples = addExamples(pronoun, requestExamples(req.query.examples)) + } + return res.json(pronoun); +}); + +export default router; diff --git a/server/routes/user.js b/server/routes/user.js index 4b29bafb..5d520f5e 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -2,13 +2,15 @@ import { Router } from 'express'; import SQL from 'sql-template-strings'; import {ulid} from "ulid"; import {buildDict, makeId, now} from "../../src/helpers"; -import translations from "../translations"; import jwt from "../../src/jwt"; import mailer from "../../src/mailer"; -import config from '../config'; +import { loadSuml } from '../loader'; import avatar from '../avatar'; import { config as socialLoginConfig, handlers as socialLoginHandlers } from '../social'; +const config = loadSuml('config'); +const translations = loadSuml('translations'); + const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-'; const normalise = s => s.trim().toLowerCase(); diff --git a/server/translations.js b/server/translations.js deleted file mode 100644 index 7eeeafe6..00000000 --- a/server/translations.js +++ /dev/null @@ -1,3 +0,0 @@ -import Suml from 'suml'; -const fs = require('fs'); -export default new Suml().parse(fs.readFileSync('./data/translations.suml').toString()); diff --git a/src/classes.js b/src/classes.js index 9450620c..e19839d2 100644 --- a/src/classes.js +++ b/src/classes.js @@ -6,14 +6,6 @@ export class ExamplePart { this.variable = variable; this.str = str; } - - format(form) { - if (!this.variable) { - return this.str[form.plural]; - } - - return form[this.str[form.plural]]; - } } export class Example { @@ -44,12 +36,12 @@ export class Example { return parts; } - format(form) { - return Example.ucfirst(this.parts.map(part => part.format(form)).join('')); - } + format(pronoun) { + const plural = this.isHonorific ? pronoun.pluralHonorific[0] : pronoun.plural[0]; - static ucfirst(str) { - return str[0].toUpperCase() + str.slice(1); + return capitalise(this[plural ? 'pluralParts' : 'singularParts'].map(part => { + return part.variable ? pronoun.getMorpheme(part.str) : part.str; + }).join('')); } }