archive.social/app/server.js

216 wiersze
5.4 KiB
JavaScript

/**
* archive.social
* @module server.js
* @author The Harvard Library Innovation Lab
* @license MIT
*/
import fs from "fs";
import assert from "assert";
import { validate as uuidValidate } from 'uuid';
import nunjucks from "nunjucks";
import { AccessKeys, SuccessLog, TwitterCapture } from "./utils/index.js";
import {
TEMPLATES_PATH,
STATIC_PATH,
MAX_PARALLEL_CAPTURES_TOTAL,
MAX_PARALLEL_CAPTURES_PER_ACCESS_KEY,
} from "./const.js";
/**
* Keeps track of how many capture processes are currently running.
* May be used to redirect users if over capacity.
*
* [!] This needs to be upgraded to proper rate limiting after launch.
*
* @type {{
* currentTotal: number,
* maxTotal: number,
* currentByAccessKey: object.<string, number>,
* maxPerAccessKey: number
* }}
*/
const CAPTURES_WATCH = {
currentTotal: 0,
maxTotal: MAX_PARALLEL_CAPTURES_TOTAL,
currentByAccessKey: {},
maxPerAccessKey: MAX_PARALLEL_CAPTURES_PER_ACCESS_KEY,
}
/**
* Frozen copy of currently valid access keys.
* [!] For this alpha: app needs to be restarted for changes to be into account.
*/
const ACCESS_KEYS = AccessKeys.fetch();
export default async function (fastify, opts) {
// Adds support for `application/x-www-form-urlencoded`
fastify.register(import('@fastify/formbody'));
// Serves files from STATIC_PATH
fastify.register(import('@fastify/static'), {
root: STATIC_PATH,
prefix: '/static/',
});
/**
* [GET] /
* Shows the landing page / form.
*/
fastify.get('/', async (request, reply) => {
const html = nunjucks.render(`${TEMPLATES_PATH}index.njk`);
return reply
.code(200)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
});
fastify.get('/test', async (request, reply) => {
const html = nunjucks.render(`${TEMPLATES_PATH}success.njk`, {
pdfBase64: fs.readFileSync("download.pdf").toString("base64"),
url: "https://twitter.com/doctorow/status/1591759999323492358"
});
return reply
.code(200)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
});
/**
* [POST] /
* Processes a request to capture a twitter url.
* Renders success page with PDF if capture went through.
* Returns to form with specific error code, passed as `errorReason`, otherwise.
*
*/
fastify.post('/', async (request, reply) => {
const data = request.body;
const accessKey = data["access-key"];
request.log.info(`Capture capacity: ${CAPTURES_WATCH.currentTotal} / ${CAPTURES_WATCH.maxTotal}.`);
//
// Check access key
//
try {
assert(uuidValidate(accessKey));
assert(ACCESS_KEYS[accessKey]);
}
catch(err) {
const html = nunjucks.render(`${TEMPLATES_PATH}index.njk`, {
error: true,
errorReason: "ACCESS-KEY"
});
return reply
.code(401)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
}
//
// Check url
//
try {
const url = new URL(data.url);
assert(url.origin === "https://twitter.com");
}
catch(err) {
const html = nunjucks.render(`${TEMPLATES_PATH}index.njk`, {
error: true,
errorReason: "URL"
});
return reply
.code(400)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
}
//
// Check that there is still capture capacity (total)
//
if (CAPTURES_WATCH.currentTotal >= CAPTURES_WATCH.maxTotal) {
const html = nunjucks.render(`${TEMPLATES_PATH}index.njk`, {
error: true,
errorReason: "TOO-MANY-CAPTURES-TOTAL"
});
return reply
.code(503)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
}
//
// Check that there is still capture capacity (for this access key)
//
if (CAPTURES_WATCH.currentByAccessKey[accessKey] >= CAPTURES_WATCH.maxPerAccessKey) {
const html = nunjucks.render(`${TEMPLATES_PATH}index.njk`, {
error: true,
errorReason: "TOO-MANY-CAPTURES-USER"
});
return reply
.code(429)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
}
//
// Process capture request
//
try {
CAPTURES_WATCH.currentTotal += 1;
if (accessKey in CAPTURES_WATCH.currentByAccessKey) {
CAPTURES_WATCH.currentByAccessKey[accessKey] += 1;
}
else {
CAPTURES_WATCH.currentByAccessKey[accessKey] = 1;
}
const tweets = new TwitterCapture(data.url);
const pdf = await tweets.capture();
SuccessLog.add(accessKey, pdf);
const html = nunjucks.render(`${TEMPLATES_PATH}success.njk`, {
pdfBase64: pdf.toString('base64'),
url: tweets.url
});
return reply
.code(200)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
}
catch(err) {
request.log.error(`Capture failed. ${err}`);
const html = nunjucks.render(`${TEMPLATES_PATH}index.njk`, {
error: true,
errorReason: "CAPTURE-ISSUE"
});
return reply
.code(500)
.header('Content-Type', 'text/html; charset=utf-8')
.send(html);
}
// In any case: we need to decrease CAPTURES_WATCH counts.
finally {
CAPTURES_WATCH.currentTotal -= 1;
if (accessKey && accessKey in CAPTURES_WATCH.currentByAccessKey) {
CAPTURES_WATCH.currentByAccessKey[data["access-key"]] -= 1;
}
}
});
};