const jsl = require("svjsl"); const fs = require("fs-extra"); const http = require("http"); var httpServer = require("./httpServer"); // module loading order is a bit fucked, so this module has to be loaded multiple times const parseJokes = require("./parseJokes"); const logRequest = require("./logRequest"); const convertFileFormat = require("./fileFormatConverter"); const analytics = require("./analytics"); const parseURL = require("./parseURL"); const meter = require("./meter"); const tr = require("./translate"); const settings = require("../settings"); const fileFormatConverter = require("./fileFormatConverter"); jsl.unused(http, analytics, tr); /** @typedef {parseJokes.SingleJoke|parseJokes.TwopartJoke} JokeSubmission */ /** @typedef {import("./types/jokes").Joke} Joke */ /** * To be called when a joke is submitted * @param {http.ServerResponse} res * @param {String} data * @param {String} fileFormat * @param {String} ip * @param {(analytics.AnalyticsDocsRequest|analytics.AnalyticsSuccessfulRequest|analytics.AnalyticsRateLimited|analytics.AnalyticsError|analytics.AnalyticsSubmission)} analyticsObject * @param {Boolean} dryRun Set to true to not add the joke to the joke file after validating it */ function jokeSubmission(res, data, fileFormat, ip, analyticsObject, dryRun) { try { if(typeof dryRun != "boolean") dryRun = false; if(typeof httpServer == "object" && Object.keys(httpServer).length <= 0) httpServer = require("./httpServer"); let submittedJoke = JSON.parse(data); let langCode = (submittedJoke.lang || settings.languages.defaultLanguage).toString().toLowerCase(); if(jsl.isEmpty(submittedJoke)) return httpServer.respondWithError(res, 105, 400, fileFormat, tr(langCode, "requestBodyIsInvalid"), langCode); let invalidChars = data.match(settings.jokes.submissions.invalidCharRegex); let invalidCharsStr = invalidChars ? invalidChars.map(ch => `0x${ch.charCodeAt(0).toString(16)}`).join(", ") : null; if(invalidCharsStr && invalidChars.length > 0) return httpServer.respondWithError(res, 109, 400, fileFormat, tr(langCode, "invalidChars", invalidCharsStr), langCode); if(submittedJoke.formatVersion == parseJokes.jokeFormatVersion && submittedJoke.formatVersion == settings.jokes.jokesFormatVersion) { // format version is correct, validate joke now let validationResult = parseJokes.validateSingle(submittedJoke, langCode); if(Array.isArray(validationResult)) return httpServer.respondWithError(res, 105, 400, fileFormat, tr(langCode, "submittedJokeFormatInvalid", validationResult.join("\n")), langCode); else if(validationResult === true) { // joke is valid, find file name and then write to file let sanitizedIP = ip.replace(settings.httpServer.ipSanitization.regex, settings.httpServer.ipSanitization.replaceChar).substring(0, 8); let curUnix = new Date().getTime(); let fileName = `${settings.jokes.jokeSubmissionPath}${langCode}/submission_${sanitizedIP}_0_${curUnix}.json`; let iter = 0; let findNextNum = currentNum => { iter++; if(iter >= settings.httpServer.rateLimiting) { logRequest("ratelimited", `IP: ${ip}`, analyticsObject); return httpServer.respondWithError(res, 101, 429, fileFormat, tr(langCode, "rateLimited", settings.httpServer.rateLimiting, settings.httpServer.timeFrame)); } if(fs.existsSync(`${settings.jokes.jokeSubmissionPath}submission_${sanitizedIP}_${currentNum}_${curUnix}.json`)) return findNextNum(currentNum + 1); else return currentNum; }; fs.ensureDirSync(`${settings.jokes.jokeSubmissionPath}${langCode}`); if(fs.existsSync(`${settings.jokes.jokeSubmissionPath}${fileName}`)) fileName = `${settings.jokes.jokeSubmissionPath}${langCode}/submission_${sanitizedIP}_${findNextNum()}_${curUnix}.json`; try { // file name was found, write to file now: if(dryRun) { let respObj = { error: false, message: tr(langCode, "dryRunSuccessful", parseJokes.jokeFormatVersion, submittedJoke.formatVersion), timestamp: new Date().getTime() }; return httpServer.pipeString(res, fileFormatConverter.auto(fileFormat, respObj, langCode), parseURL.getMimeTypeFromFileFormatString(fileFormat), 201); } return writeJokeToFile(res, fileName, submittedJoke, fileFormat, ip, analyticsObject, langCode); } catch(err) { return httpServer.respondWithError(res, 100, 500, fileFormat, tr(langCode, "errWhileSavingSubmission", err), langCode); } } } else { return httpServer.respondWithError(res, 105, 400, fileFormat, tr(langCode, "wrongFormatVersion", parseJokes.jokeFormatVersion, submittedJoke.formatVersion), langCode); } } catch(err) { return httpServer.respondWithError(res, 105, 400, fileFormat, tr(settings.languages.defaultLanguage, "invalidJSON", err), settings.languages.defaultLanguage); } } /** * Writes a joke to a json file * @param {http.ServerResponse} res * @param {String} filePath * @param {JokeSubmission} submittedJoke * @param {String} fileFormat * @param {String} ip * @param {(analytics.AnalyticsDocsRequest|analytics.AnalyticsSuccessfulRequest|analytics.AnalyticsRateLimited|analytics.AnalyticsError|analytics.AnalyticsSubmission)} analyticsObject * @param {String} [langCode] */ function writeJokeToFile(res, filePath, submittedJoke, fileFormat, ip, analyticsObject, langCode) { if(typeof httpServer == "object" && Object.keys(httpServer).length <= 0) httpServer = require("./httpServer"); let reformattedJoke = reformatJoke(submittedJoke); fs.writeFile(filePath, JSON.stringify(reformattedJoke, null, 4), err => { if(!err) { // successfully wrote to file let responseObj = { "error": false, "message": tr(langCode, "submissionSaved"), "submission": reformattedJoke, "timestamp": new Date().getTime() }; meter.update("submission", 1); let submissionObject = analyticsObject; submissionObject.submission = reformattedJoke; logRequest("submission", ip, submissionObject); return httpServer.pipeString(res, convertFileFormat.auto(fileFormat, responseObj, langCode), parseURL.getMimeTypeFromFileFormatString(fileFormat), 201); } // error while writing to file else return httpServer.respondWithError(res, 100, 500, fileFormat, tr(langCode, "errWhileSavingSubmission", err), langCode); }); } /** * Coarse filter that ensures that a joke is formatted as expected. * This doesn't do any validation and omits missing properties! * @param {Joke|JokeSubmission} joke * @returns {Joke|JokeSubmission} Returns the reformatted joke */ function reformatJoke(joke) { let retJoke = {}; if(joke.formatVersion) retJoke.formatVersion = joke.formatVersion; retJoke = { ...retJoke, category: typeof joke.category === "string" ? parseJokes.resolveCategoryAlias(joke.category) : joke.category, type: joke.type }; if(joke.type == "single") { retJoke.joke = joke.joke; } else if(joke.type == "twopart") { retJoke.setup = joke.setup; retJoke.delivery = joke.delivery; } retJoke.flags = { nsfw: joke.flags.nsfw, religious: joke.flags.religious, political: joke.flags.political, racist: joke.flags.racist, sexist: joke.flags.sexist, explicit: joke.flags.explicit, }; if(joke.lang) retJoke.lang = joke.lang; if(typeof retJoke.lang === "string") retJoke.lang = retJoke.lang.toLowerCase(); retJoke.safe = joke.safe || false; if(joke.id) retJoke.id = joke.id; return retJoke; } module.exports = jokeSubmission; module.exports.reformatJoke = reformatJoke;