2020-12-18 02:34:58 -08:00
|
|
|
import { Router } from 'express';
|
|
|
|
import SQL from 'sql-template-strings';
|
|
|
|
import sha1 from 'sha1';
|
|
|
|
import {ulid} from "ulid";
|
2021-02-02 07:49:49 -08:00
|
|
|
import Papa from 'papaparse';
|
2021-06-09 05:47:08 -07:00
|
|
|
import {handleErrorAsync} from "../../src/helpers";
|
2022-02-28 06:18:54 -08:00
|
|
|
import {intersection, difference} from "../../src/sets";
|
2020-12-18 02:34:58 -08:00
|
|
|
|
2021-02-01 02:17:26 -08:00
|
|
|
const getIp = req => {
|
|
|
|
try {
|
|
|
|
return req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ips.join(',') || req.ip;
|
|
|
|
} catch {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-18 02:34:58 -08:00
|
|
|
const buildFingerprint = req => sha1(`
|
2021-02-01 02:17:26 -08:00
|
|
|
${getIp(req)}
|
2020-12-18 02:34:58 -08:00
|
|
|
${req.headers['user-agent']}
|
|
|
|
${req.headers['accept-language']}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const hasFinished = async req => {
|
|
|
|
if (req.user) {
|
|
|
|
const byUser = await req.db.get(SQL`
|
|
|
|
SELECT * FROM census
|
2021-06-17 16:10:59 -07:00
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
2020-12-18 02:34:58 -08:00
|
|
|
AND userId = ${req.user.id}
|
|
|
|
`);
|
2021-02-01 01:35:06 -08:00
|
|
|
return !!byUser;
|
2020-12-18 02:34:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
const fingerprint = buildFingerprint(req);
|
|
|
|
const byFingerprint = await req.db.get(SQL`
|
|
|
|
SELECT * FROM census
|
2021-06-17 16:10:59 -07:00
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
2020-12-18 02:34:58 -08:00
|
|
|
AND fingerprint = ${fingerprint}
|
2021-02-01 01:41:13 -08:00
|
|
|
AND userId IS NULL
|
2020-12-18 02:34:58 -08:00
|
|
|
`);
|
2021-02-01 01:41:13 -08:00
|
|
|
return !!byFingerprint;
|
2020-12-18 02:34:58 -08:00
|
|
|
}
|
|
|
|
|
2022-02-17 10:07:59 -08:00
|
|
|
const isRelevant = (answers) => {
|
|
|
|
return global.config.census.relevant.includes(answers['0']);
|
|
|
|
}
|
|
|
|
|
|
|
|
const isTroll = (answers, writins) => {
|
|
|
|
if (Object.values(writins).filter(x => !!x).length) {
|
|
|
|
return null; // unknown, send to moderation
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i in global.config.census.questions) {
|
|
|
|
if (global.config.census.questions[i].type === 'textarea' && answers[i.toString()]) {
|
|
|
|
return null; // unknown, send to moderation
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false; // no free-text provided
|
|
|
|
}
|
|
|
|
|
2020-12-18 02:34:58 -08:00
|
|
|
const router = Router();
|
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.get('/census/finished', handleErrorAsync(async (req, res) => {
|
2020-12-18 02:34:58 -08:00
|
|
|
return res.json(await hasFinished(req));
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-12-18 02:34:58 -08:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/census/submit', handleErrorAsync(async (req, res) => {
|
2022-02-17 10:07:59 -08:00
|
|
|
const answers = JSON.parse(req.body.answers);
|
|
|
|
const writins = JSON.parse(req.body.writins);
|
2020-12-18 02:34:58 -08:00
|
|
|
|
|
|
|
const id = ulid();
|
2022-02-17 10:07:59 -08:00
|
|
|
await req.db.get(SQL`INSERT INTO census (id, locale, edition, userId, fingerprint, answers, writins, suspicious, relevant, troll) VALUES (
|
2020-12-18 02:34:58 -08:00
|
|
|
${id},
|
2021-06-17 16:10:59 -07:00
|
|
|
${global.config.locale},
|
|
|
|
${global.config.census.edition},
|
2020-12-18 02:34:58 -08:00
|
|
|
${req.user ? req.user.id : null},
|
|
|
|
${buildFingerprint(req)},
|
|
|
|
${req.body.answers},
|
2021-02-01 02:02:57 -08:00
|
|
|
${req.body.writins},
|
2022-02-17 10:07:59 -08:00
|
|
|
${await hasFinished(req)},
|
|
|
|
${isRelevant(answers) ? 1 : 0},
|
|
|
|
${isTroll(answers, writins) ? 1 : 0}
|
2020-12-18 02:34:58 -08:00
|
|
|
)`);
|
|
|
|
|
|
|
|
return res.json(id);
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-12-18 02:34:58 -08:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.get('/census/count', handleErrorAsync(async (req, res) => {
|
2022-02-17 08:00:57 -08:00
|
|
|
if (!req.isGranted('census')) {
|
|
|
|
return res.json({
|
|
|
|
all: 0,
|
|
|
|
nonbinary: 0,
|
|
|
|
usable: 0,
|
|
|
|
awaiting: 0,
|
|
|
|
});
|
|
|
|
}
|
2022-02-17 06:19:33 -08:00
|
|
|
|
|
|
|
// duplication reason: https://github.com/felixfbecker/node-sql-template-strings/issues/71
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
all: (await req.db.get(SQL`
|
|
|
|
SELECT COUNT(*) as c FROM census
|
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
|
|
|
`)).c,
|
|
|
|
nonbinary: (await req.db.get(SQL`
|
|
|
|
SELECT COUNT(*) as c FROM census
|
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
2022-02-17 10:07:59 -08:00
|
|
|
AND relevant = 1
|
2022-02-17 06:19:33 -08:00
|
|
|
`)).c,
|
|
|
|
usable: (await req.db.get(SQL`
|
|
|
|
SELECT COUNT(*) as c FROM census
|
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
2022-02-17 10:07:59 -08:00
|
|
|
AND relevant = 1
|
2022-02-17 06:19:33 -08:00
|
|
|
AND troll = 0
|
|
|
|
`)).c,
|
|
|
|
awaiting: (await req.db.get(SQL`
|
|
|
|
SELECT COUNT(*) as c FROM census
|
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
|
|
|
AND troll IS NULL
|
|
|
|
`)).c,
|
|
|
|
});
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-12-18 08:41:01 -08:00
|
|
|
|
2022-02-28 06:18:54 -08:00
|
|
|
const calculateAggregate = (config, answer) => {
|
|
|
|
const expected = new Set(config.values);
|
|
|
|
if (config.exclusive && difference(answer, expected).size > 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
switch (config.operation) {
|
|
|
|
case 'OR':
|
|
|
|
return intersection(expected, answer).size > 0;
|
|
|
|
case 'AND':
|
|
|
|
return intersection(expected, answer).size === expected.size;
|
|
|
|
default:
|
|
|
|
throw new Exception(`Operation "${config.operation} not supported"`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.get('/census/export', handleErrorAsync(async (req, res) => {
|
2021-02-02 07:49:49 -08:00
|
|
|
if (!req.isGranted('census')) {
|
2021-12-02 10:11:04 -08:00
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
2021-02-02 07:49:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
const report = [];
|
2022-02-17 06:19:33 -08:00
|
|
|
for (let {answers, writins, troll} of await req.db.all(SQL`
|
2021-02-02 07:49:49 -08:00
|
|
|
SELECT answers, writins FROM census
|
2021-06-17 16:10:59 -07:00
|
|
|
WHERE locale = ${global.config.locale}
|
2022-02-17 06:19:33 -08:00
|
|
|
AND edition = ${global.config.census.edition}
|
|
|
|
AND suspicious = 0
|
|
|
|
AND troll = 0
|
2021-02-02 07:49:49 -08:00
|
|
|
`)) {
|
|
|
|
answers = JSON.parse(answers);
|
|
|
|
writins = JSON.parse(writins);
|
|
|
|
|
|
|
|
const answer = {};
|
|
|
|
let i = 0;
|
|
|
|
for (let question of config.census.questions) {
|
|
|
|
if (question.type === 'checkbox') {
|
2022-02-28 06:18:54 -08:00
|
|
|
const answerForAggregate = new Set();
|
2021-02-02 07:49:49 -08:00
|
|
|
for (let [option, comment] of question.options) {
|
2022-02-28 06:18:54 -08:00
|
|
|
const checked = (answers[i.toString()] || []).includes(option);
|
|
|
|
answer[`${i}_${option}`] = checked ? 1 : '';
|
|
|
|
if (checked) {
|
|
|
|
answerForAggregate.add(option);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let aggr in question.aggregates || {}) {
|
|
|
|
if (!question.aggregates.hasOwnProperty(aggr)) { continue; }
|
|
|
|
answer[`${i}_aggr_${aggr}`] = calculateAggregate(question.aggregates[aggr], answerForAggregate) ? 1 : '';
|
2021-02-02 07:49:49 -08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
answer[`${i}_`] = answers[i.toString()] || '';
|
|
|
|
}
|
|
|
|
if (question.writein) {
|
2021-02-02 08:42:40 -08:00
|
|
|
answer[`${i}__writein`] = writins[i.toString()] || '';
|
2021-02-02 07:49:49 -08:00
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
report.push(answer);
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.set('content-type', 'text/csv').send(Papa.unparse(report));
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2021-02-02 07:49:49 -08:00
|
|
|
|
2022-02-17 06:19:33 -08:00
|
|
|
router.get('/census/moderation/queue', handleErrorAsync(async (req, res) => {
|
|
|
|
if (!req.isGranted('census')) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
const queue = await req.db.all(SQL`
|
|
|
|
SELECT id, answers, writins FROM census
|
|
|
|
WHERE locale = ${global.config.locale}
|
|
|
|
AND edition = ${global.config.census.edition}
|
|
|
|
AND troll IS NULL
|
|
|
|
ORDER BY RANDOM()
|
|
|
|
`);
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
count: queue.length,
|
|
|
|
next: queue.length ? queue[0] : null,
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
|
|
|
router.post('/census/moderation/decide', handleErrorAsync(async (req, res) => {
|
|
|
|
if (!req.isGranted('census')) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
const queue = await req.db.get(SQL`
|
|
|
|
UPDATE census SET troll = ${parseInt(req.body.decision)} WHERE id = ${req.body.id}
|
|
|
|
`);
|
|
|
|
|
|
|
|
return res.json('ok');
|
|
|
|
}));
|
|
|
|
|
2020-12-18 02:34:58 -08:00
|
|
|
export default router;
|