jokeSubmission.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. const jsl = require("svjsl");
  2. const fs = require("fs-extra");
  3. const http = require("http");
  4. var httpServer = require("./httpServer"); // module loading order is a bit fucked, so this module has to be loaded multiple times
  5. const parseJokes = require("./parseJokes");
  6. const logRequest = require("./logRequest");
  7. const convertFileFormat = require("./fileFormatConverter");
  8. const analytics = require("./analytics");
  9. const parseURL = require("./parseURL");
  10. const meter = require("./meter");
  11. const tr = require("./translate");
  12. const settings = require("../settings");
  13. const fileFormatConverter = require("./fileFormatConverter");
  14. jsl.unused(http, analytics, tr);
  15. /** @typedef {parseJokes.SingleJoke|parseJokes.TwopartJoke} JokeSubmission */
  16. /** @typedef {import("./types/jokes").Joke} Joke */
  17. /**
  18. * To be called when a joke is submitted
  19. * @param {http.ServerResponse} res
  20. * @param {String} data
  21. * @param {String} fileFormat
  22. * @param {String} ip
  23. * @param {(analytics.AnalyticsDocsRequest|analytics.AnalyticsSuccessfulRequest|analytics.AnalyticsRateLimited|analytics.AnalyticsError|analytics.AnalyticsSubmission)} analyticsObject
  24. * @param {Boolean} dryRun Set to true to not add the joke to the joke file after validating it
  25. */
  26. function jokeSubmission(res, data, fileFormat, ip, analyticsObject, dryRun)
  27. {
  28. try
  29. {
  30. if(typeof dryRun != "boolean")
  31. dryRun = false;
  32. if(typeof httpServer == "object" && Object.keys(httpServer).length <= 0)
  33. httpServer = require("./httpServer");
  34. let submittedJoke = JSON.parse(data);
  35. let langCode = (submittedJoke.lang || settings.languages.defaultLanguage).toString().toLowerCase();
  36. if(jsl.isEmpty(submittedJoke))
  37. return httpServer.respondWithError(res, 105, 400, fileFormat, tr(langCode, "requestBodyIsInvalid"), langCode);
  38. let invalidChars = data.match(settings.jokes.submissions.invalidCharRegex);
  39. let invalidCharsStr = invalidChars ? invalidChars.map(ch => `0x${ch.charCodeAt(0).toString(16)}`).join(", ") : null;
  40. if(invalidCharsStr && invalidChars.length > 0)
  41. return httpServer.respondWithError(res, 109, 400, fileFormat, tr(langCode, "invalidChars", invalidCharsStr), langCode);
  42. if(submittedJoke.formatVersion == parseJokes.jokeFormatVersion && submittedJoke.formatVersion == settings.jokes.jokesFormatVersion)
  43. {
  44. // format version is correct, validate joke now
  45. let validationResult = parseJokes.validateSingle(submittedJoke, langCode);
  46. if(Array.isArray(validationResult))
  47. return httpServer.respondWithError(res, 105, 400, fileFormat, tr(langCode, "submittedJokeFormatInvalid", validationResult.join("\n")), langCode);
  48. else if(validationResult === true)
  49. {
  50. // joke is valid, find file name and then write to file
  51. let sanitizedIP = ip.replace(settings.httpServer.ipSanitization.regex, settings.httpServer.ipSanitization.replaceChar).substring(0, 8);
  52. let curUnix = new Date().getTime();
  53. let fileName = `${settings.jokes.jokeSubmissionPath}${langCode}/submission_${sanitizedIP}_0_${curUnix}.json`;
  54. let iter = 0;
  55. let findNextNum = currentNum => {
  56. iter++;
  57. if(iter >= settings.httpServer.rateLimiting)
  58. {
  59. logRequest("ratelimited", `IP: ${ip}`, analyticsObject);
  60. return httpServer.respondWithError(res, 101, 429, fileFormat, tr(langCode, "rateLimited", settings.httpServer.rateLimiting, settings.httpServer.timeFrame));
  61. }
  62. if(fs.existsSync(`${settings.jokes.jokeSubmissionPath}submission_${sanitizedIP}_${currentNum}_${curUnix}.json`))
  63. return findNextNum(currentNum + 1);
  64. else return currentNum;
  65. };
  66. fs.ensureDirSync(`${settings.jokes.jokeSubmissionPath}${langCode}`);
  67. if(fs.existsSync(`${settings.jokes.jokeSubmissionPath}${fileName}`))
  68. fileName = `${settings.jokes.jokeSubmissionPath}${langCode}/submission_${sanitizedIP}_${findNextNum()}_${curUnix}.json`;
  69. try
  70. {
  71. // file name was found, write to file now:
  72. if(dryRun)
  73. {
  74. let respObj = {
  75. error: false,
  76. message: tr(langCode, "dryRunSuccessful", parseJokes.jokeFormatVersion, submittedJoke.formatVersion),
  77. timestamp: new Date().getTime()
  78. };
  79. return httpServer.pipeString(res, fileFormatConverter.auto(fileFormat, respObj, langCode), parseURL.getMimeTypeFromFileFormatString(fileFormat), 201);
  80. }
  81. return writeJokeToFile(res, fileName, submittedJoke, fileFormat, ip, analyticsObject, langCode);
  82. }
  83. catch(err)
  84. {
  85. return httpServer.respondWithError(res, 100, 500, fileFormat, tr(langCode, "errWhileSavingSubmission", err), langCode);
  86. }
  87. }
  88. }
  89. else
  90. {
  91. return httpServer.respondWithError(res, 105, 400, fileFormat, tr(langCode, "wrongFormatVersion", parseJokes.jokeFormatVersion, submittedJoke.formatVersion), langCode);
  92. }
  93. }
  94. catch(err)
  95. {
  96. return httpServer.respondWithError(res, 105, 400, fileFormat, tr(settings.languages.defaultLanguage, "invalidJSON", err), settings.languages.defaultLanguage);
  97. }
  98. }
  99. /**
  100. * Writes a joke to a json file
  101. * @param {http.ServerResponse} res
  102. * @param {String} filePath
  103. * @param {JokeSubmission} submittedJoke
  104. * @param {String} fileFormat
  105. * @param {String} ip
  106. * @param {(analytics.AnalyticsDocsRequest|analytics.AnalyticsSuccessfulRequest|analytics.AnalyticsRateLimited|analytics.AnalyticsError|analytics.AnalyticsSubmission)} analyticsObject
  107. * @param {String} [langCode]
  108. */
  109. function writeJokeToFile(res, filePath, submittedJoke, fileFormat, ip, analyticsObject, langCode)
  110. {
  111. if(typeof httpServer == "object" && Object.keys(httpServer).length <= 0)
  112. httpServer = require("./httpServer");
  113. let reformattedJoke = reformatJoke(submittedJoke);
  114. fs.writeFile(filePath, JSON.stringify(reformattedJoke, null, 4), err => {
  115. if(!err)
  116. {
  117. // successfully wrote to file
  118. let responseObj = {
  119. "error": false,
  120. "message": tr(langCode, "submissionSaved"),
  121. "submission": reformattedJoke,
  122. "timestamp": new Date().getTime()
  123. };
  124. meter.update("submission", 1);
  125. let submissionObject = analyticsObject;
  126. submissionObject.submission = reformattedJoke;
  127. logRequest("submission", ip, submissionObject);
  128. return httpServer.pipeString(res, convertFileFormat.auto(fileFormat, responseObj, langCode), parseURL.getMimeTypeFromFileFormatString(fileFormat), 201);
  129. }
  130. // error while writing to file
  131. else return httpServer.respondWithError(res, 100, 500, fileFormat, tr(langCode, "errWhileSavingSubmission", err), langCode);
  132. });
  133. }
  134. /**
  135. * Coarse filter that ensures that a joke is formatted as expected.
  136. * This doesn't do any validation and omits missing properties!
  137. * @param {Joke|JokeSubmission} joke
  138. * @returns {Joke|JokeSubmission} Returns the reformatted joke
  139. */
  140. function reformatJoke(joke)
  141. {
  142. let retJoke = {};
  143. if(joke.formatVersion)
  144. retJoke.formatVersion = joke.formatVersion;
  145. retJoke = {
  146. ...retJoke,
  147. category: typeof joke.category === "string" ? parseJokes.resolveCategoryAlias(joke.category) : joke.category,
  148. type: joke.type
  149. };
  150. if(joke.type == "single")
  151. {
  152. retJoke.joke = joke.joke;
  153. }
  154. else if(joke.type == "twopart")
  155. {
  156. retJoke.setup = joke.setup;
  157. retJoke.delivery = joke.delivery;
  158. }
  159. retJoke.flags = {
  160. nsfw: joke.flags.nsfw,
  161. religious: joke.flags.religious,
  162. political: joke.flags.political,
  163. racist: joke.flags.racist,
  164. sexist: joke.flags.sexist,
  165. explicit: joke.flags.explicit,
  166. };
  167. if(joke.lang)
  168. retJoke.lang = joke.lang;
  169. if(typeof retJoke.lang === "string")
  170. retJoke.lang = retJoke.lang.toLowerCase();
  171. retJoke.safe = joke.safe || false;
  172. if(joke.id)
  173. retJoke.id = joke.id;
  174. return retJoke;
  175. }
  176. module.exports = jokeSubmission;
  177. module.exports.reformatJoke = reformatJoke;