parseJokes.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. // this module parses all the jokes to verify that they are valid and that their structure is not messed up
  2. const fs = require("fs-extra");
  3. const jsl = require("svjsl");
  4. const settings = require("../settings");
  5. const debug = require("./verboseLogging");
  6. const languages = require("./languages");
  7. const AllJokes = require("./classes/AllJokes");
  8. const tr = require("./translate");
  9. /**
  10. * @typedef {Object} CategoryAlias
  11. * @prop {String} alias Name of the alias
  12. * @prop {String} value The value this alias resolves to
  13. */
  14. /** @type {CategoryAlias[]} */
  15. var categoryAliases = [];
  16. /** @type {number|undefined} */
  17. let jokeFormatVersion;
  18. /**
  19. * Parses all jokes
  20. * @returns {Promise<Boolean>}
  21. */
  22. function init()
  23. {
  24. return new Promise((resolve, reject) => {
  25. // prepare category aliases
  26. Object.keys(settings.jokes.possible.categoryAliases).forEach(alias => {
  27. let aliasResolved = settings.jokes.possible.categoryAliases[alias];
  28. if(!settings.jokes.possible.categories.includes(aliasResolved))
  29. return reject(`Error while setting up category aliases: The resolved value "${aliasResolved}" of alias "${alias}" is not present in the "settings.jokes.possible.categories" array.`);
  30. categoryAliases.push({ alias, value: aliasResolved });
  31. });
  32. debug("JokeParser", `Registered ${categoryAliases.length} category aliases`);
  33. // prepare jokes files
  34. let jokesFiles = fs.readdirSync(settings.jokes.jokesFolderPath);
  35. let result = [];
  36. let allJokesFilesObj = {};
  37. let outerPromises = [];
  38. let parsedJokesAmount = 0;
  39. jokesFiles.forEach(jf => {
  40. if(jf == settings.jokes.jokesTemplateFile)
  41. return;
  42. outerPromises.push(new Promise((resolveOuter, rejectOuter) => {
  43. jsl.unused(rejectOuter);
  44. let fileNameValid = (fileName) => {
  45. if(!fileName.endsWith(".json"))
  46. return false;
  47. let spl1 = fileName.split(".json")[0];
  48. if(spl1.includes("-") && languages.isValidLang(spl1.split("-")[1]) === true && spl1.split("-")[0] == "jokes")
  49. return true;
  50. return false;
  51. };
  52. let getLangCode = (fileName) => {
  53. if(!fileName.endsWith(".json"))
  54. return false;
  55. let spl1 = fileName.split(".json")[0];
  56. if(spl1.includes("-") && languages.isValidLang(spl1.split("-")[1]) === true)
  57. return spl1.split("-")[1].toLowerCase();
  58. };
  59. let langCode = getLangCode(jf);
  60. if(!jf.endsWith(".json") || !fileNameValid(jf))
  61. result.push(`${jsl.colors.fg.red}Error: Invalid file "${settings.jokes.jokesFolderPath}${jf}" found. It has to follow this pattern: "jokes-xy.json"`);
  62. fs.readFile(`${settings.jokes.jokesFolderPath}${jf}`, (err, jokesFile) => {
  63. if(err)
  64. return reject(err);
  65. try
  66. {
  67. jokesFile = JSON.parse(jokesFile.toString());
  68. }
  69. catch(err)
  70. {
  71. return reject(`Error while parsing jokes file "${jf}" as JSON: ${err}`);
  72. }
  73. //#MARKER format version
  74. if(jokesFile.info.formatVersion == settings.jokes.jokesFormatVersion)
  75. result.push(true);
  76. else result.push(`Joke file format version of language "${langCode}" is set to "${jokesFile.info.formatVersion}" - Expected: "${settings.jokes.jokesFormatVersion}"`);
  77. jokesFile.jokes.forEach((joke, i) => {
  78. //#MARKER joke ID
  79. if(!jsl.isEmpty(joke.id) && !isNaN(parseInt(joke.id)))
  80. result.push(true);
  81. else result.push(`Joke with index/ID ${i} of language "${langCode}" doesn't have an "id" property or it is invalid`);
  82. //#MARKER category
  83. if(settings.jokes.possible.categories.map(c => c.toLowerCase()).includes(joke.category.toLowerCase()))
  84. result.push(true);
  85. else
  86. result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid category (Note: aliases are not allowed here)`);
  87. //#MARKER type and actual joke
  88. if(joke.type == "single")
  89. {
  90. if(!jsl.isEmpty(joke.joke))
  91. result.push(true);
  92. else result.push(`Joke with index/ID ${i} of language "${langCode}" doesn't have a "joke" property`);
  93. }
  94. else if(joke.type == "twopart")
  95. {
  96. if(!jsl.isEmpty(joke.setup))
  97. result.push(true);
  98. else result.push(`Joke with index/ID ${i} of language "${langCode}" doesn't have a "setup" property`);
  99. if(!jsl.isEmpty(joke.delivery))
  100. result.push(true);
  101. else result.push(`Joke with index/ID ${i} of language "${langCode}" doesn't have a "delivery" property`);
  102. }
  103. else result.push(`Joke with index/ID ${i} of language "${langCode}" doesn't have a "type" property or it is invalid`);
  104. //#MARKER flags
  105. if(!jsl.isEmpty(joke.flags))
  106. {
  107. if(!jsl.isEmpty(joke.flags.nsfw) || (joke.flags.nsfw !== false && joke.flags.nsfw !== true))
  108. result.push(true);
  109. else result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid "NSFW" flag`);
  110. if(!jsl.isEmpty(joke.flags.racist) || (joke.flags.racist !== false && joke.flags.racist !== true))
  111. result.push(true);
  112. else result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid "racist" flag`);
  113. if(!jsl.isEmpty(joke.flags.sexist) || (joke.flags.sexist !== false && joke.flags.sexist !== true))
  114. result.push(true);
  115. else result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid "sexist" flag`);
  116. if(!jsl.isEmpty(joke.flags.political) || (joke.flags.political !== false && joke.flags.political !== true))
  117. result.push(true);
  118. else result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid "political" flag`);
  119. if(!jsl.isEmpty(joke.flags.religious) || (joke.flags.religious !== false && joke.flags.religious !== true))
  120. result.push(true);
  121. else result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid "religious" flag`);
  122. if(!jsl.isEmpty(joke.flags.explicit) || (joke.flags.explicit !== false && joke.flags.explicit !== true))
  123. result.push(true);
  124. else result.push(`Joke with index/ID ${i} of language "${langCode}" has an invalid "explicit" flag`);
  125. }
  126. else result.push(`Joke with index/ID ${i} of language "${langCode}" doesn't have a "flags" object or it is invalid`);
  127. parsedJokesAmount++;
  128. });
  129. allJokesFilesObj[langCode] = jokesFile;
  130. return resolveOuter();
  131. });
  132. }));
  133. });
  134. Promise.all(outerPromises).then(() => {
  135. let errors = [];
  136. result.forEach(res => {
  137. if(typeof res === "string")
  138. errors.push(res);
  139. });
  140. let allJokesObj = new AllJokes(allJokesFilesObj);
  141. let formatVersions = [settings.jokes.jokesFormatVersion];
  142. languages.jokeLangs().map(jl => jl.code).sort().forEach(lang => {
  143. formatVersions.push(allJokesObj.getFormatVersion(lang));
  144. });
  145. if(!jsl.allEqual(formatVersions))
  146. errors.push(`One or more of the jokes files has an invalid format version`);
  147. module.exports.allJokes = allJokesObj;
  148. module.exports.jokeCount = allJokesObj.getJokeCount();
  149. module.exports.jokeCountPerLang = allJokesObj.getJokeCountPerLang();
  150. module.exports.safeJokes = allJokesObj.getSafeJokes();
  151. let fmtVer = allJokesObj.getFormatVersion("en");
  152. module.exports.jokeFormatVersion = fmtVer;
  153. jokeFormatVersion = fmtVer;
  154. debug("JokeParser", `Done parsing all ${parsedJokesAmount} jokes. Errors: ${errors.length === 0 ? jsl.colors.fg.green : jsl.colors.fg.red}${errors.length}${jsl.colors.rst}`);
  155. if(jsl.allEqual(result) && result[0] === true && errors.length === 0)
  156. return resolve();
  157. return reject(`Errors:\n- ${errors.join("\n- ")}`);
  158. }).catch(err => {
  159. return reject(err);
  160. });
  161. });
  162. }
  163. /**
  164. * @typedef {"Misc"|"Programming"|"Dark"|"Pun"|"Spooky"|"Christmas"} JokeCategory Resolved category name (not an alias)
  165. */
  166. /**
  167. * @typedef {"Miscellaneous"|"Coding"|"Development"|"Halloween"} JokeCategoryAlias Category name aliases
  168. */
  169. /**
  170. * @typedef {Object} SingleJoke A joke of type single
  171. * @prop {JokeCategory} category The category of the joke
  172. * @prop {"single"} type The type of the joke
  173. * @prop {String} joke The joke itself
  174. * @prop {Object} flags
  175. * @prop {Boolean} flags.nsfw Whether the joke is NSFW or not
  176. * @prop {Boolean} flags.racist Whether the joke is racist or not
  177. * @prop {Boolean} flags.religious Whether the joke is religiously offensive or not
  178. * @prop {Boolean} flags.political Whether the joke is politically offensive or not
  179. * @prop {Boolean} flags.explicit Whether the joke contains explicit language
  180. * @prop {Number} id The ID of the joke
  181. * @prop {String} lang The language of the joke
  182. */
  183. /**
  184. * @typedef {Object} TwopartJoke A joke of type twopart
  185. * @prop {JokeCategory} category The category of the joke
  186. * @prop {"twopart"} type The type of the joke
  187. * @prop {String} setup The setup of the joke
  188. * @prop {String} delivery The delivery of the joke
  189. * @prop {Object} flags
  190. * @prop {Boolean} flags.nsfw Whether the joke is NSFW or not
  191. * @prop {Boolean} flags.racist Whether the joke is racist or not
  192. * @prop {Boolean} flags.religious Whether the joke is religiously offensive or not
  193. * @prop {Boolean} flags.political Whether the joke is politically offensive or not
  194. * @prop {Boolean} flags.explicit Whether the joke contains explicit language
  195. * @prop {Number} id The ID of the joke
  196. * @prop {String} lang The language of the joke
  197. */
  198. /**
  199. * Validates a single joke passed as a parameter
  200. * @param {(SingleJoke|TwopartJoke)} joke A joke object of type single or twopart
  201. * @param {String} lang Language code
  202. * @returns {(Boolean|Array<String>)} Returns true if the joke has the correct format, returns string array containing error(s) if invalid
  203. */
  204. function validateSingle(joke, lang)
  205. {
  206. let jokeErrors = [];
  207. if(languages.isValidLang(lang) !== true)
  208. jokeErrors.push(tr(lang, "parseJokesInvalidLanguageCode"));
  209. // reserialize object
  210. if(typeof joke == "object")
  211. joke = JSON.stringify(joke);
  212. joke = JSON.parse(joke);
  213. // TODO: version 2.3.2:
  214. // let jokeObj = {
  215. // "formatVersion": true,
  216. // "category": true,
  217. // "type": true,
  218. // };
  219. // if(joke.type == "single")
  220. // jokeObj.joke = true;
  221. // else if(joke.type == "twopart")
  222. // {
  223. // jokeObj.setup = true;
  224. // jokeObj.delivery = true;
  225. // }
  226. // jokeObj = {
  227. // ...jokeObj,
  228. // flags: {
  229. // nsfw: true,
  230. // religious: true,
  231. // political: true,
  232. // racist: true,
  233. // sexist: true
  234. // },
  235. // lang: true
  236. // }
  237. try
  238. {
  239. //#MARKER format version
  240. if(joke.formatVersion != null)
  241. {
  242. if(joke.formatVersion != settings.jokes.jokesFormatVersion || joke.formatVersion != jokeFormatVersion)
  243. {
  244. jokeErrors.push(tr(lang, "parseJokesFormatVersionMismatch", joke.formatVersion, jokeFormatVersion));
  245. // jokeObj.formatVersion = false; // TODO: version 2.3.2: repeat this for everything below
  246. }
  247. }
  248. else jokeErrors.push(tr(lang, "parseJokesNoFormatVersionOrInvalid"));
  249. //#MARKER type and actual joke
  250. if(joke.type == "single")
  251. {
  252. if(jsl.isEmpty(joke.joke))
  253. jokeErrors.push(tr(lang, "parseJokesSingleNoJokeProperty"));
  254. }
  255. else if(joke.type == "twopart")
  256. {
  257. if(jsl.isEmpty(joke.setup))
  258. jokeErrors.push(tr(lang, "parseJokesTwopartNoSetupProperty"));
  259. if(jsl.isEmpty(joke.delivery))
  260. jokeErrors.push(tr(lang, "parseJokesTwopartNoDeliveryProperty"));
  261. }
  262. else jokeErrors.push(tr(lang, "parseJokesNoTypeProperty"));
  263. //#MARKER joke category
  264. let jokeCat = typeof joke.category === "string" ? resolveCategoryAlias(joke.category) : joke.category;
  265. if(joke.category == null)
  266. jokeErrors.push(tr(lang, "parseJokesNoCategoryProperty"));
  267. else if(typeof jokeCat !== "string")
  268. jokeErrors.push(tr(lang, "parseJokesInvalidCategory"));
  269. else
  270. {
  271. let categoryValid = false;
  272. settings.jokes.possible.categories.forEach(cat => {
  273. if(jokeCat.toLowerCase() === cat.toLowerCase())
  274. categoryValid = true;
  275. });
  276. if(!categoryValid)
  277. jokeErrors.push(tr(lang, "parseJokesInvalidCategory"));
  278. }
  279. //#MARKER flags
  280. if(!jsl.isEmpty(joke.flags))
  281. {
  282. if(jsl.isEmpty(joke.flags.nsfw) || (joke.flags.nsfw !== false && joke.flags.nsfw !== true))
  283. jokeErrors.push(tr(lang, "parseJokesNoFlagNsfw"));
  284. if(jsl.isEmpty(joke.flags.racist) || (joke.flags.racist !== false && joke.flags.racist !== true))
  285. jokeErrors.push(tr(lang, "parseJokesNoFlagRacist"));
  286. if(jsl.isEmpty(joke.flags.sexist) || (joke.flags.sexist !== false && joke.flags.sexist !== true))
  287. jokeErrors.push(tr(lang, "parseJokesNoFlagSexist"));
  288. if(jsl.isEmpty(joke.flags.political) || (joke.flags.political !== false && joke.flags.political !== true))
  289. jokeErrors.push(tr(lang, "parseJokesNoFlagPolitical"));
  290. if(jsl.isEmpty(joke.flags.religious) || (joke.flags.religious !== false && joke.flags.religious !== true))
  291. jokeErrors.push(tr(lang, "parseJokesNoFlagReligious"));
  292. if(jsl.isEmpty(joke.flags.explicit) || (joke.flags.explicit !== false && joke.flags.explicit !== true))
  293. jokeErrors.push(tr(lang, "parseJokesNoFlagExplicit"));
  294. }
  295. else jokeErrors.push(tr(lang, "parseJokesNoFlagsObject"));
  296. //#MARKER lang
  297. if(jsl.isEmpty(joke.lang))
  298. jokeErrors.push(tr(lang, "parseJokesNoLangProperty"));
  299. let langV = languages.isValidLang(joke.lang, lang);
  300. if(typeof langV === "string")
  301. jokeErrors.push(tr(lang, "parseJokesLangPropertyInvalid", langV));
  302. else if(langV !== true)
  303. jokeErrors.push(tr(lang, "parseJokesNoLangProperty"));
  304. }
  305. catch(err)
  306. {
  307. jokeErrors.push(tr(lang, "parseJokesCantParse", err.toString()));
  308. }
  309. if(jsl.isEmpty(jokeErrors))
  310. return true;
  311. else
  312. return jokeErrors;
  313. }
  314. /**
  315. * Returns the resolved value of a joke category alias or returns the initial value if it isn't an alias or is invalid
  316. * @param {JokeCategory|JokeCategoryAlias} category A singular joke category or joke category alias
  317. * @returns {JokeCategory}
  318. */
  319. function resolveCategoryAlias(category)
  320. {
  321. let cat = category;
  322. categoryAliases.forEach(catAlias => {
  323. if(typeof category !== "string")
  324. throw new TypeError(`Can't resolve category alias because '${category}' is not of type string`);
  325. if(category.toLowerCase() == catAlias.alias.toLowerCase())
  326. cat = catAlias.value;
  327. });
  328. return cat;
  329. }
  330. /**
  331. * Returns the resolved values of an array of joke category aliases or returns the initial values if there are none
  332. * @param {JokeCategory[]|JokeCategoryAlias[]} categories An array of joke categories (can contain aliases)
  333. * @returns {JokeCategory[]}
  334. */
  335. function resolveCategoryAliases(categories)
  336. {
  337. return categories.map(cat => resolveCategoryAlias(cat));
  338. }
  339. module.exports = { init, validateSingle, resolveCategoryAlias, resolveCategoryAliases }