Greasy Fork is available in English.

Quest Reader

Makes it more convenient to read quests

La data de 31-07-2019. Vezi ultima versiune.

  1. // ==UserScript==
  2. // @name Quest Reader
  3. // @author naileD
  4. // @namespace QuestReader
  5. // @include *//tgchan.org/kusaba/quest/res/*
  6. // @include *//tgchan.org/kusaba/questarch/res/*
  7. // @include *//tgchan.org/kusaba/graveyard/res/*
  8. // @description Makes it more convenient to read quests
  9. // @version 1
  10. // @grant none
  11. // @icon data:image/vnd.microsoft.icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsSAAALEgHS3X78AAAANklEQVQokWNgoBOI2mJKpEomMvQgNAxRPUy4JGjjJJqoZoSrZmBgWOZzGlk/mlKILBMafxAAAE1pG/UEXzMMAAAAAElFTkSuQmCC
  12. // ==/UserScript==
  13. "use strict";
  14. //entry point is more or less at the end of the script
  15.  
  16. //enum
  17. const PostType = { UPDATE: 0, AUTHORCOMMENT: 1, SUGGESTION: 2, COMMENT: 3 }
  18.  
  19. //UpdateAnalyzer class
  20. //Input: document of the quest
  21. //Output: a Map object with all the quest's posts, where keys are post IDs and values are post types. The post types are Update (0), AuthorComment (1), Suggestion (2), Comment (3); There's no comments... yet.
  22. //Usage: var results = new UpdateAnalyzer().processQuest(document);
  23. class UpdateAnalyzer {
  24. constructor(options) {
  25. this.regex = UpdateAnalyzer.getRegexes();
  26. if (options) {
  27. this.postCache = null; //Used to transfer posts cache to/from this class. Used for debugging purposes.
  28. this.useCache = options.useCache; //Used for debugging purposes.
  29. this.debug = options.debug;
  30. this.debugAfterDate = options.debugAfterDate;
  31. }
  32. }
  33.  
  34. analyzeQuest(questDoc) {
  35. var posts = !this.postCache ? this.getPosts(questDoc) : JSON.parse(new TextDecoder().decode(this.postCache));
  36. var authorID = posts[0].userID; //authodID is the userID of the first post
  37. this.threadID = posts[0].postID; //threadID is the postID of the first post
  38.  
  39. this.totalFiles = this.getTotalFileCount(posts);
  40. var questFixes = this.getFixes(this.threadID); //for quests where we can't correctly determine authors and updates, we use a built-in database of fixes
  41. if (this.debug && (questFixes.imageQuest !== undefined || Object.values(questFixes).some(fix => Object.values(fix).length > 0))) { console.log(`Quest has manual fixes`); console.log(questFixes); }
  42. var graphData = this.getUserGraphData(posts, questFixes, authorID); //get user names as nodes and edges for building user graph
  43. var users = this.buildUserGraph(graphData.nodes, graphData.edges); //build a basic user graph... whatever that means!
  44. this.author = this.find(users[authorID]);
  45. this.getUserPostAndFileCounts(posts, users, questFixes); //count the amount of posts and files each user made
  46. this.imageQuest = this.isImageQuest(questFixes); //image quest is when the author posts files at least 50% of the time
  47. if (this.debug) console.log(`Image quest: ${this.imageQuest}`);
  48. if (this.imageQuest) { //in case this is an image quest, merge users a bit differently
  49. users = this.buildUserGraph(graphData.nodes, graphData.edges, graphData.strongNodes, authorID); //build the user graph again, but with some restrictions
  50. this.author = this.find(users[authorID]);
  51. this.processFilePosts(posts, users, questFixes); //analyze file names and merge users based on when one file name is predicted from another
  52. this.getUserPostAndFileCounts(posts, users, questFixes); //count the amount of posts and files each user posted
  53. this.mergeCommonFilePosters(posts, users, questFixes); //merge certain file-posting users with the quest author
  54. this.mergeMajorityFilePoster(posts, users, questFixes); //consider a user who posted 50%+ of the files in the thread as the author
  55. }
  56. this.setPostUsers(posts, users, questFixes); //do final user resolution
  57. var postTypes = this.getFinalPostTypes(posts, questFixes); //determine which posts are updates
  58. return postTypes;
  59. }
  60.  
  61. getPosts(questDoc) {
  62. var defaultName = "Suggestion";
  63. var posts = {}; //dictionary => postID / post object
  64. questDoc.querySelectorAll(".postwidth").forEach(postHeaderElement => { //querySelectorAll is FASTER than getElementsByClassName when DOM is large
  65. var postID = parseInt(postHeaderElement.querySelector("span[id^=dnb]").id.split("-")[2]);
  66. if (posts[postID]) { //checking this may seem unnecessary, but it's required for compatibility with some imageboard scripts
  67. return;
  68. }
  69. var uid, name, trip, subject, fileElement, fileExt, fileName = "", activeContent, contentElement;
  70. var uidElement = postHeaderElement.querySelector(".uid");
  71. uid = uidElement.textContent.substring(4);
  72. trip = postHeaderElement.querySelector(".postertrip");
  73. if (trip) { //use tripcode instead of name if it exists
  74. name = trip.textContent;
  75. }
  76. else {
  77. name = postHeaderElement.querySelector(".postername").textContent.trim();
  78. name = name == defaultName ? "" : name.toLowerCase();
  79. }
  80. subject = postHeaderElement.querySelector(".filetitle");
  81. subject = subject ? subject.textContent.trim() : "";
  82. fileElement = postHeaderElement.querySelector(".filesize");
  83. if (fileElement) { //try to get the original file name
  84. fileName = fileElement.getElementsByTagName("a")[0].href;
  85. var match = fileName.match(this.regex.fileExtension);
  86. fileExt = match ? match[0].toLowerCase() : "";
  87. if (fileExt == ".png" || fileExt == ".gif" || fileExt == ".jpg" || fileExt == ".jpeg") {
  88. var fileInfo = fileElement.lastChild.textContent.split(", ");
  89. if (fileInfo.length >= 3) {
  90. fileName = fileInfo[2].split("\n")[0];
  91. }
  92. }
  93. else {
  94. fileName = fileName.substr(fileName.lastIndexOf("/") + 1); //couldn't find original file name, use file name from the server instead
  95. }
  96. fileName = fileName.replace(this.regex.fileExtension, ""); //ignore file's extension
  97. }
  98. contentElement = postHeaderElement.nextElementSibling;
  99. activeContent = contentElement.querySelector("img, iframe") ? true : false; //does a post contain icons
  100. var postData = { postID: postID, userID: uid, userName: name, fileName: fileName, activeContent: activeContent };
  101. if (this.useCache) {
  102. postData.textUpdate = this.regex.fraction.test(subject) || this.containsQuotes(contentElement);
  103. }
  104. else {
  105. postData.subject = subject;
  106. postData.contentElement = contentElement;
  107. }
  108. if (this.useCache || this.debug || this.debugAfterDate) {
  109. postData.date = Date.parse(postHeaderElement.querySelector("label").lastChild.nodeValue);
  110. }
  111. posts[postID] = postData;
  112. });
  113. var postsArray = Object.values(posts); //convert to an array
  114. if (this.useCache) { //We stringify the object into JSON and then encode it into a Uint8Array to save space, otherwise the database would be too large
  115. this.postCache = new TextEncoder().encode(Object.toJSON ? Object.toJSON(postsArray) : JSON.stringify(postsArray)); //JSON.stringify stringifies twice. Another TGchan's protoaculous bug.
  116. }
  117. return postsArray;
  118. }
  119.  
  120. getTotalFileCount(posts) {
  121. var totalFileCount = 0;
  122. posts.forEach(post => { if (post.fileName || post.activeContent) totalFileCount++; });
  123. return totalFileCount;
  124. }
  125.  
  126. isImageQuest(questFixes, ignore) {
  127. if (questFixes.imageQuest !== undefined) {
  128. return questFixes.imageQuest;
  129. }
  130. else {
  131. return (this.author.fileCount / this.author.postCount) >= 0.5;
  132. }
  133. }
  134.  
  135. getUserGraphData(posts, questFixes, authorID) {
  136. var graphData = { nodes: new Set(), strongNodes: new Set(), edges: {} };
  137. posts.forEach(post => {
  138. graphData.nodes.add(post.userID);
  139. if (post.userName) {
  140. graphData.nodes.add(post.userName);
  141. graphData.edges[`${post.userID}${post.userName}`] = { E1: post.userID, E2: post.userName };
  142. }
  143. if (post.fileName || post.activeContent) { //strong nodes are user IDs that posted files
  144. graphData.strongNodes.add(post.userID);
  145. if (post.userName) {
  146. graphData.strongNodes.add(post.userName);
  147. }
  148. if (post.fileName && post.activeContent && post.userID != authorID) { //users that made posts with both file and icons are most likely the author
  149. graphData.edges[`${authorID}${post.userID}`] = { E1: authorID, E2: post.userID, hint: "fileAndIcons" };
  150. }
  151. }
  152. });
  153. for (var missedID in questFixes.missedAuthors) { //add missing links to the author from manual fixes
  154. graphData.edges[`${authorID}${missedID}`] = { E1: authorID, E2: missedID, hint: "missedAuthors" };
  155. graphData.strongNodes.add(missedID);
  156. }
  157. graphData.edges = Object.values(graphData.edges);
  158. return graphData;
  159. }
  160.  
  161. buildUserGraph(nodes, edges, strongNodes, authorID) {
  162. var users = {};
  163. var edgesSet = new Set(edges);
  164. nodes.forEach(node => {
  165. users[node] = this.makeSet(node);
  166. });
  167. if (!strongNodes) {
  168. edgesSet.forEach(edge => this.union(users[edge.E1], users[edge.E2]));
  169. }
  170. else {
  171. edgesSet.forEach(edge => { //merge strong with strong and weak with weak
  172. if ((strongNodes.has(edge.E1) && strongNodes.has(edge.E2)) || (!strongNodes.has(edge.E1) && !strongNodes.has(edge.E2))) {
  173. this.union(users[edge.E1], users[edge.E2]);
  174. edgesSet.delete(edge);
  175. }
  176. });
  177. var author = this.find(users[authorID]);
  178. edgesSet.forEach(edge => { //merge strong with weak, but only for users which aren't the author
  179. if (this.find(users[edge.E1]) != author && this.find(users[edge.E2]) != author) {
  180. this.union(users[edge.E1], users[edge.E2]);
  181. }
  182. });
  183. }
  184. return users;
  185. }
  186.  
  187. processFilePosts(posts, users, questFixes) {
  188. var last2Files = new Map();
  189. var filePosts = posts.filter(post => post.fileName && !questFixes.wrongImageUpdates[post.postID]);
  190. filePosts.forEach(post => {
  191. var postUser = this.find(users[post.userID]);
  192. var postFileName = post.fileName.match(this.regex.lastNumber) ? post.fileName : null; //no use processing files without numbers
  193. if (post.userName && this.find(users[post.userName]) == this.author) {
  194. postUser = this.author;
  195. }
  196. if (!last2Files.has(postUser)) {
  197. last2Files.set(postUser, [ null, null ]);
  198. }
  199. last2Files.get(postUser).shift();
  200. last2Files.get(postUser).push(postFileName);
  201. last2Files.forEach((last2, user) => {
  202. if (user == postUser) {
  203. return;
  204. }
  205. if ((last2[0] !== null && this.fileNamePredicts(last2[0], post.fileName)) || (last2[1] !== null && this.fileNamePredicts(last2[1], post.fileName))) {
  206. if (this.debug || (this.debugAfterDate && this.debugAfterDate < post.date)) {
  207. console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} merged (file name) ${postUser.id} with ${user.id} (author: ${this.author.id})`);
  208. }
  209. var mergedUser = this.union(user, postUser);
  210. last2Files.delete(user.parent != user ? user : postUser);
  211. last2Files.get(mergedUser).shift();
  212. last2Files.get(mergedUser).push(postFileName);
  213. if (this.find(this.author) == mergedUser) {
  214. this.author = mergedUser;
  215. }
  216. }
  217. });
  218. });
  219. return true;
  220. }
  221.  
  222. getUserPostAndFileCounts(posts, users, questFixes) {
  223. for (var userID in users) {
  224. users[userID].postCount = 0;
  225. users[userID].fileCount = 0;
  226. }
  227. posts.forEach(post => {
  228. var user = this.decidePostUser(post, users, questFixes);
  229. user.postCount++;
  230. if (post.fileName || post.activeContent) {
  231. user.fileCount++;
  232. }
  233. });
  234. }
  235.  
  236. fileNamePredicts(fileName1, fileName2) {
  237. var match1 = fileName1.match(this.regex.lastNumber);
  238. var match2 = fileName2.match(this.regex.lastNumber);
  239. if (!match1 || !match2) {
  240. return false;
  241. }
  242. var indexDifference = match2.index - match1.index;
  243. if (indexDifference > 1 || indexDifference < -1) {
  244. return false;
  245. }
  246. var numberDifference = parseInt(match2[1]) - parseInt(match1[1]);
  247. if (numberDifference !== 2 && numberDifference !== 1) {
  248. return false;
  249. }
  250. var name1 = fileName1.replace(this.regex.lastNumber, "");
  251. var name2 = fileName2.replace(this.regex.lastNumber, "");
  252. return this.stringsAreSimilar(name1, name2);
  253. }
  254.  
  255. stringsAreSimilar(string1, string2) {
  256. var lengthDiff = string1.length - string2.length;
  257. if (lengthDiff > 1 || lengthDiff < -1) {
  258. return false;
  259. }
  260. var s1 = lengthDiff > 0 ? string1 : string2;
  261. var s2 = lengthDiff > 0 ? string2 : string1;
  262. for (var i = 0, j = 0, diff = 0; i < s1.length; i++, j++) {
  263. if (s1[i] !== s2[j]) {
  264. diff++;
  265. if (diff === 2) {
  266. return false;
  267. }
  268. if (lengthDiff !== 0) {
  269. j--;
  270. }
  271. }
  272. }
  273. return true;
  274. }
  275.  
  276. mergeMajorityFilePoster(posts, users, questFixes) {
  277. if (this.author.fileCount > this.totalFiles / 2) {
  278. return;
  279. }
  280. for (var userID in users) {
  281. if (users[userID].fileCount >= this.totalFiles / 2 && users[userID] != this.author) {
  282. if (this.debug || (this.debugAfterDate && this.debugAfterDate < posts[posts.length - 1].date)) {
  283. console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html merged majority file poster ${users[userID].id} ${(100 * users[userID].fileCount / this.totalFiles).toFixed(1)}%`);
  284. }
  285. var parent = this.union(this.author, users[userID]);
  286. var child = users[userID].parent != users[userID] ? users[userID] : this.author;
  287. parent.fileCount += child.fileCount;
  288. parent.postCount += child.postCount;
  289. this.author = parent;
  290. return;
  291. }
  292. }
  293. }
  294.  
  295. mergeCommonFilePosters(posts, users, questFixes) {
  296. var merged = [];
  297. var filteredUsers = Object.values(users).filter(user => user.parent == user && user.fileCount >= 3 && user.fileCount / user.postCount > 0.5 && user != this.author);
  298. var usersSet = new Set(filteredUsers);
  299. posts.forEach(post => {
  300. if ((post.fileName || post.activeContent) && !questFixes.wrongImageUpdates[post.postID] && this.isTextPostAnUpdate(post)) {
  301. for (var user of usersSet) {
  302. if (this.find(users[post.userID]) == user) {
  303. if (this.debug || (this.debugAfterDate && this.debugAfterDate < post.date)) {
  304. console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html new common poster ${users[post.userID].id}`);
  305. }
  306. var parent = this.union(this.author, user);
  307. var child = user.parent != user ? user : this.author;
  308. parent.fileCount += child.fileCount;
  309. parent.postCount += child.postCount;
  310. this.author = parent;
  311. usersSet.delete(user);
  312. break;
  313. }
  314. }
  315. }
  316. });
  317. }
  318.  
  319. setPostUsers(posts, users, questFixes) {
  320. posts.forEach(post => {
  321. post.user = this.decidePostUser(post, users, questFixes);
  322. });
  323. }
  324.  
  325. decidePostUser(post, users, questFixes) {
  326. var user = this.find(users[post.userID]);
  327. if (post.userName) {
  328. if (questFixes.ignoreTextPosts[post.userName]) { //choose to the one that isn't the author
  329. if (user == this.author) {
  330. user = this.find(users[post.userName]);
  331. }
  332. }
  333. else if (this.find(users[post.userName]) == this.author) { //choose the one that is the author
  334. user = this.author;
  335. }
  336. }
  337. return user;
  338. }
  339.  
  340. getFinalPostTypes(posts, questFixes) {
  341. // Updates are posts made by the author and, in case of image quests, author posts that contain files or icons
  342. var postTypes = new Map();
  343. posts.forEach(post => {
  344. var postType = PostType.SUGGESTION;
  345. if (post.user == this.author) {
  346. if (post.fileName || post.activeContent) { //image post
  347. if (!questFixes.wrongImageUpdates[post.postID]) {
  348. postType = PostType.UPDATE;
  349. }
  350. else if (!questFixes.ignoreTextPosts[post.userID] && !questFixes.ignoreTextPosts[post.userName]) {
  351. postType = PostType.AUTHORCOMMENT;
  352. }
  353. }
  354. else if (!questFixes.ignoreTextPosts[post.userID] && !questFixes.ignoreTextPosts[post.userName]) { //text post
  355. if (!questFixes.wrongTextUpdates[post.postID] && (!this.imageQuest || this.isTextPostAnUpdate(post))) {
  356. postType = PostType.UPDATE;
  357. }
  358. else {
  359. postType = PostType.AUTHORCOMMENT;
  360. }
  361. }
  362. if (questFixes.missedTextUpdates[post.postID]) {
  363. postType = PostType.UPDATE;
  364. }
  365. }
  366. if (this.debugAfterDate && this.debugAfterDate < post.date) {
  367. if (postType == PostType.SUGGESTION && post.fileName) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new non-update`);
  368. if (postType == PostType.AUTHORCOMMENT) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new author comment`);
  369. if (postType == PostType.UPDATE && this.imageQuest && !post.fileName && !post.activeContent) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new text update`);
  370. }
  371. postTypes.set(post.postID, postType);
  372. });
  373. return postTypes;
  374. }
  375.  
  376. isTextPostAnUpdate(post) {
  377. if (post.textUpdate === undefined) {
  378. post.textUpdate = this.regex.fraction.test(post.subject) || this.containsQuotes(post.contentElement);
  379. }
  380. return post.textUpdate;
  381. }
  382.  
  383. containsQuotes(contentElement) {
  384. //extract post's text, but ignore text inside spoilers, links, dice rolls or any sort of brackets
  385. var filteredContentText = "";
  386. contentElement.childNodes.forEach(node => {
  387. if (node.className !== "spoiler" && node.nodeName != "A" && (node.nodeName != "B" || !this.regex.diceRoll.test(node.textContent))) {
  388. filteredContentText += node.textContent;
  389. }
  390. });
  391. filteredContentText = filteredContentText.replace(this.regex.bracketedTexts, "").trim();
  392. //if the post contains dialogue, then it's likely to be an update
  393. var quotedTexts = filteredContentText.match(this.regex.quotedTexts) || [];
  394. for (let q of quotedTexts) {
  395. if (this.regex.endsWithPunctuation.test(q)) {
  396. return true;
  397. }
  398. }
  399. return false;
  400. }
  401.  
  402. makeSet(id) {
  403. var node = { id: id, children: [] };
  404. node.parent = node;
  405. return node;
  406. }
  407.  
  408. find(node) { //find with path halving
  409. while (node.parent != node) {
  410. var curr = node;
  411. node = node.parent;
  412. curr.parent = node.parent;
  413. }
  414. return node;
  415. }
  416.  
  417. union(node1, node2) {
  418. var node1root = this.find(node1);
  419. var node2root = this.find(node2);
  420. if (node1root == node2root) {
  421. return node1root;
  422. }
  423. node2root.parent = node1root;
  424. node1root.children.push(node2root); //having a list of children isn't a part of Union-Find, but it makes debugging much easier
  425. node2root.children.forEach(child => node1root.children.push(child));
  426. return node1root;
  427. }
  428.  
  429. static getRegexes() {
  430. if (!this.regex) { //cache as a static class property
  431. this.regex = {
  432. fileExtension: new RegExp("[.][^.]+$"), //finds ".png" in "image.png"
  433. lastNumber: new RegExp("([0-9]+)(?=[^0-9]*$)"), //finds "50" in "image50.png"
  434. fraction: new RegExp("[0-9][ ]*/[ ]*[0-9]"), //finds "1/4" in "Update 1/4"
  435. diceRoll: new RegExp("^rolled [0-9].* = [0-9]+$"), //finds "rolled 10, 20 = 30"
  436. quotedTexts: new RegExp("[\"“”][^\"“”]*[\"“”]","gu"), //finds text inside quotes
  437. endsWithPunctuation: new RegExp("[.,!?][ ]*[\"“”]$"), //finds if a quote ends with a punctuation
  438. bracketedTexts: new RegExp("(\\([^)]*\\))|(\\[[^\\]]*\\])|(\\{[^}]*\\})|(<[^>]*>)", "gu"), //finds text within various kinds of brackets... looks funny
  439. canonID: new RegExp("^[0-9a-f]{6}$")
  440. };
  441. }
  442. return this.regex;
  443. }
  444.  
  445. getFixes(threadID) {
  446. var fixes = UpdateAnalyzer.getAllFixes()[threadID] || {};
  447. //convert array values to lower case and then into object properties for faster access
  448. for (let prop of [ "missedAuthors", "missedTextUpdates", "wrongTextUpdates", "wrongImageUpdates", "ignoreTextPosts" ]) {
  449. if (!fixes[prop]) {
  450. fixes[prop] = { };
  451. }
  452. else if (Array.isArray(fixes[prop])) { //can't use Array.reduce() because tgchan's js library protoaculous destroyed it
  453. fixes[prop] = fixes[prop].reduceRight((acc, el) => { if (!el.startsWith("!")) el = el.toLowerCase(); acc[el] = true; return acc; }, { });
  454. }
  455. }
  456. return fixes;
  457. }
  458.  
  459. // Manual fixes. In some cases it's simply impossible (impractical) to automatically determine which posts are updates. So we fix those rare cases manually.
  460. // list last updated on:
  461. // 2019/07/27
  462.  
  463. //missedAuthors: User IDs which should be linked to the author. Either because the automation failed, or the quest has guest authors / is a collaboration. Guest authors also usually need an entry under ignoreTextPosts.
  464. //ignoreTextPosts: User IDs of which text posts should not be set as author comments. It happens when a suggester shares an ID with the author and this suggester makes a text post. Or if the guest authors make suggestions.
  465. //(An empty ignoreTextPosts string matches posts with an empty/default poster name)
  466. //missedImageUpdates: Actually, no such fixes exist. All missed image update posts are added through adding author IDs to missedAuthors.
  467. //missedTextUpdates: Post IDs of text-only posts which are not author comments, but quest updates. It happens when authors make text updates in image quests. Or forget to attach an image to the update post.
  468. //wrongImageUpdates: Post IDs of image posts which are not quest updates. It happens when a suggester shares an ID with the author(s) and this suggester makes an image post. Or a guest author posts a non-update image post.
  469. //wrongTextUpdates: Post IDs of text-only posts which were misidentified as updates. It happens when an author comment contains a valid quote and the script accidentally thinks some dialogue is going on.
  470. //imageQuest: Forcefully set quest type. It happens when the automatically-determined quest type is incorrect. Either because of too many image updates in a text quest, or text updates in an image quest.
  471. //(Also, if most of the author's text posts in an image quest are updates, then it's sometimes simpler to set the quest as a text quest, rather than picking them out one by one.)
  472. static getAllFixes() {
  473. if (!this.allFixes) {
  474. this.allFixes = { //cache as a static class property
  475. 12: { missedAuthors: [ "!g9Qfmdqho2" ] },
  476. 26: { ignoreTextPosts: [ "Coriell", "!DHEj4YTg6g" ] },
  477. 101: { wrongTextUpdates: [ "442" ] },
  478. 171: { wrongTextUpdates: [ "1402" ] },
  479. 504: { missedTextUpdates: [ "515", "597", "654", "1139", "1163", "1180", "7994", "9951" ] },
  480. 998: { ignoreTextPosts: [ "" ] },
  481. 1292: { missedAuthors: [ "Chaptermaster II" ], missedTextUpdates: [ "1311", "1315", "1318" ], ignoreTextPosts: [ "" ] },
  482. 1702: { wrongImageUpdates: [ "2829" ] },
  483. 3090: { ignoreTextPosts: [ "", "!94Ud9yTfxQ", "Glaive" ], wrongImageUpdates: [ "3511", "3574", "3588", "3591", "3603", "3612" ] },
  484. 4602: { missedTextUpdates: [ "4630", "6375" ] },
  485. 7173: { missedTextUpdates: [ "8515", "10326" ] },
  486. 8906: { missedTextUpdates: [ "9002", "9009" ] },
  487. 9190: { missedAuthors: [ "!OeZ2B20kbk" ], missedTextUpdates: [ "26073" ] },
  488. 13595: { wrongTextUpdates: [ "18058" ] },
  489. 16114: { missedTextUpdates: [ "20647" ] },
  490. 17833: { ignoreTextPosts: [ "!swQABHZA/E" ] },
  491. 19308: { missedTextUpdates: [ "19425", "19600", "19912" ] },
  492. 19622: { wrongImageUpdates: [ "30710", "30719", "30732", "30765" ] },
  493. 19932: { missedTextUpdates: [ "20038", "20094", "20173", "20252" ] },
  494. 20501: { ignoreTextPosts: [ "bd2eec" ] },
  495. 21601: { missedTextUpdates: [ "21629", "21639" ] },
  496. 21853: { missedTextUpdates: [ "21892", "21898", "21925", "22261", "22266", "22710", "23308", "23321", "23862", "23864", "23900", "24206", "25479", "25497", "25943", "26453", "26787", "26799",
  497. "26807", "26929", "27328", "27392", "27648", "27766", "27809", "29107", "29145" ] },
  498. 22208: { missedAuthors: [ "fb5d8e" ] },
  499. 24530: { wrongImageUpdates: [ "25023" ] },
  500. 25354: { imageQuest: false},
  501. 26933: { missedTextUpdates: [ "26935", "26955", "26962", "26967", "26987", "27015", "28998" ] },
  502. 29636: { missedTextUpdates: [ "29696", "29914", "30025", "30911" ], wrongImageUpdates: [ "30973", "32955", "33107" ] },
  503. 30350: { imageQuest: false, wrongTextUpdates: [ "30595", "32354", "33704" ] },
  504. 30357: { missedTextUpdates: [ "30470", "30486", "30490", "30512", "33512" ] },
  505. 33329: { wrongTextUpdates: [ "43894" ] },
  506. 37304: { ignoreTextPosts: [ "", "GREEN", "!!x2ZmLjZmyu", "Adept", "Cruxador", "!ifOCf11HXk" ] },
  507. 37954: { missedTextUpdates: [ "41649" ] },
  508. 38276: { ignoreTextPosts: [ "!ifOCf11HXk" ] },
  509. 41510: { missedTextUpdates: [ "41550", "41746" ] },
  510. 44240: { missedTextUpdates: [ "44324", "45768", "45770", "48680", "48687" ] },
  511. 45522: { missedTextUpdates: [ "55885" ] },
  512. 45986: { missedTextUpdates: [ "45994", "46019" ] },
  513. 49306: { missedTextUpdates: [ "54246" ] },
  514. 49400: { ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  515. 49937: { missedTextUpdates: [ "52386" ] },
  516. 53129: { wrongTextUpdates: [ "53505" ] },
  517. 53585: { missedAuthors: [ "b1e366", "aba0a3", "18212a", "6756f8", "f98e0b", "1c48f4", "f4963f", "45afb1", "b94893", "135d9a" ], ignoreTextPosts: [ "", "!7BHo7QtR6I", "Test Pattern", "Rowan", "Insomnia", "!!L1ZwWyZzZ5" ] },
  518. 54766: { missedAuthors: [ "e16ca8" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  519. 55639: { wrongImageUpdates: [ "56711", "56345", "56379", "56637" ] },
  520. 56194: { wrongTextUpdates: [ "61608" ] },
  521. 59263: { missedTextUpdates: [ "64631" ] },
  522. 62091: { imageQuest: true},
  523. 65742: { missedTextUpdates: [ "66329", "66392", "67033", "67168" ] },
  524. 67058: { missedTextUpdates: [ "67191", "67685" ] },
  525. 68065: { missedAuthors: [ "7452df", "1d8589" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  526. 70887: { missedAuthors: [ "e53955", "7c9cdd", "2084ff", "064d19", "51efff", "d3c8d2" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  527. 72794: { wrongTextUpdates: [ "76740" ] },
  528. 74474: { missedAuthors: [ "309964" ] },
  529. 75425: { missedTextUpdates: [ "75450", "75463", "75464", "75472", "75490", "75505", "77245" ] },
  530. 75763: { missedAuthors: [ "068b0e" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  531. 76892: { missedTextUpdates: [ "86875", "86884", "87047", "88315" ] },
  532. 79146: { missedAuthors: [ "4a3269" ] },
  533. 79654: { missedTextUpdates: [ "83463", "83529" ] },
  534. 79782: { missedTextUpdates: [ "79975", "80045" ] },
  535. 82970: { missedTextUpdates: [ "84734" ] },
  536. 83325: { missedAuthors: [ "076064" ] },
  537. 84134: { imageQuest: false},
  538. 85235: { missedTextUpdates: [ "85257", "85282", "113215", "114739", "151976", "152022", "159250" ] },
  539. 88264: { missedAuthors: [ "3fec76", "714b9c" ] },
  540. 92605: { ignoreTextPosts: [ "" ] },
  541. 94645: { missedTextUpdates: [ "97352" ] },
  542. 95242: { missedTextUpdates: [ "95263" ] },
  543. 96023: { missedTextUpdates: [ "96242" ] },
  544. 96466: { ignoreTextPosts: [ "Reverie" ] },
  545. 96481: { imageQuest: true},
  546. 97014: { missedTextUpdates: [ "97061", "97404", "97915", "98124", "98283", "98344", "98371", "98974", "98976", "98978", "99040", "99674", "99684" ] },
  547. 99095: { wrongImageUpdates: [ "111452" ] },
  548. 99132: { ignoreTextPosts: [ "" ] },
  549. 100346: { missedTextUpdates: [ "100626", "100690", "100743", "100747", "101143", "101199", "101235", "101239" ] },
  550. 101388: { ignoreTextPosts: [ "Glaive" ] },
  551. 102433: { missedTextUpdates: [ "102519", "102559", "102758" ] },
  552. 102899: { missedTextUpdates: [ "102903" ] },
  553. 103435: { missedTextUpdates: [ "104279", "105950" ] },
  554. 103850: { ignoreTextPosts: [ "" ] },
  555. 106656: { wrongTextUpdates: [ "115606" ] },
  556. 107789: { missedTextUpdates: [ "107810", "107849", "107899" ] },
  557. 108599: { wrongImageUpdates: [ "171382", "172922", "174091", "180752", "180758" ] },
  558. 108805: { wrongImageUpdates: [ "110203" ] },
  559. 109071: { missedTextUpdates: [ "109417" ] },
  560. 112133: { missedTextUpdates: [ "134867" ] },
  561. 112414: { missedTextUpdates: [ "112455" ] },
  562. 113768: { missedAuthors: [ "e9a4f7" ] },
  563. 114133: { ignoreTextPosts: [ "" ] },
  564. 115831: { missedTextUpdates: [ "115862" ] },
  565. 119431: { ignoreTextPosts: [ "" ] },
  566. 120384: { missedAuthors: [ "233aab" ] },
  567. 126204: { imageQuest: true, missedTextUpdates: [ "127069", "127089", "161046", "161060", "161563" ] },
  568. 126248: { missedTextUpdates: [ "193064" ] },
  569. 128706: { missedAuthors: [ "2e2f06", "21b50e", "e0478c", "9c87f6", "931351", "e294f1", "749d64", "f3254a" ] },
  570. 131255: { missedTextUpdates: [ "151218" ] },
  571. 137683: { missedTextUpdates: [ "137723" ] },
  572. 139086: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  573. 139513: { missedTextUpdates: [ "139560" ] },
  574. 141257: { missedTextUpdates: [ "141263", "141290", "141513", "146287" ], ignoreTextPosts: [ "" ], wrongImageUpdates: [ "141265" ] },
  575. 146112: { missedAuthors: [ "//_emily" ] },
  576. 153225: { missedTextUpdates: [ "153615", "153875" ] },
  577. 155665: { missedTextUpdates: [ "155670", "155684", "155740" ] },
  578. 156257: { missedTextUpdates: [ "156956" ] },
  579. 157277: { missedAuthors: [ "23c8f1", "8bb533" ] },
  580. 161117: { missedTextUpdates: [ "167255", "168000" ] },
  581. 162089: { missedTextUpdates: [ "167940" ] },
  582. 164793: { missedAuthors: [ "e973f4" ], ignoreTextPosts: [ "!TEEDashxDA" ] },
  583. 165537: { missedAuthors: [ "a9f6ce" ] },
  584. 173621: { ignoreTextPosts: [ "" ] },
  585. 174398: { missedAuthors: [ "bf0d4e", "158c5c" ] },
  586. 176965: { missedTextUpdates: [ "177012" ] },
  587. 177281: { missedTextUpdates: [ "178846" ] },
  588. 181790: { ignoreTextPosts: [ "Mister Brush" ], wrongImageUpdates: [ "182280" ] },
  589. 183194: { ignoreTextPosts: [ "!CRITTerXzI" ], wrongImageUpdates: [ "183207" ] },
  590. 183637: { imageQuest: false, wrongTextUpdates: [ "183736" ] },
  591. 185345: { wrongTextUpdates: [ "185347" ] },
  592. 185579: { missedTextUpdates: [ "188091", "188697", "188731", "188748", "190868" ] },
  593. 186709: { missedTextUpdates: [ "186735" ] },
  594. 188253: { missedTextUpdates: [ "215980", "215984", "222136" ] },
  595. 188571: { missedTextUpdates: [ "188633" ] },
  596. 188970: { ignoreTextPosts: [ "" ] },
  597. 191328: { missedAuthors: [ "f54a9c", "862cf6", "af7d90", "4c1052", "e75bed", "09e145" ] },
  598. 191976: { missedAuthors: [ "20fc85" ] },
  599. 192879: { missedTextUpdates: [ "193009" ] },
  600. 193934: { missedTextUpdates: [ "212768" ] },
  601. 196310: { missedTextUpdates: [ "196401" ] },
  602. 196517: { missedTextUpdates: [ "196733" ] },
  603. 198458: { missedTextUpdates: [ "198505", "198601", "199570" ] },
  604. 200054: { missedAuthors: [ "a4b4e3" ] },
  605. 201427: { missedTextUpdates: [ "201467", "201844" ] },
  606. 203072: { missedTextUpdates: [ "203082", "203100", "206309", "207033", "208766" ] },
  607. 206945: { missedTextUpdates: [ "206950" ] },
  608. 207011: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  609. 207296: { missedTextUpdates: [ "214551" ] },
  610. 207756: { missedTextUpdates: [ "208926" ] },
  611. 209334: { missedTextUpdates: [ "209941" ] },
  612. 210613: { missedTextUpdates: [ "215711", "220853" ] },
  613. 210928: { missedTextUpdates: [ "215900" ] },
  614. 211320: { ignoreTextPosts: [ "Kindling", "Bahu" ], wrongImageUpdates: [ "211587", "215436" ] },
  615. 212584: { missedAuthors: [ "40a8d3" ] },
  616. 212915: { missedTextUpdates: [ "229550" ] },
  617. 217193: { missedAuthors: [ "7f1ecd", "c00244", "7c97d9", "8c0848", "491db1", "c2c011", "e15f89",
  618. "e31d52", "3ce5b4", "c1f2ce", "5f0943", "1dc978", "d65652", "446ab5", "f906a7", "dad664", "231806" ] },
  619. 217269: { imageQuest: false, wrongTextUpdates: [ "217860", "219314" ] },
  620. 218385: { missedAuthors: [ "812dcf" ] },
  621. 220049: { ignoreTextPosts: [ "Slinkoboy" ], wrongImageUpdates: [ "228035", "337790" ] },
  622. 222777: { imageQuest: false},
  623. 224095: { missedTextUpdates: [ "224196", "224300", "224620", "244476" ] },
  624. 233213: { missedTextUpdates: [ "233498" ], ignoreTextPosts: [ "Bahu" ] },
  625. 234437: { missedTextUpdates: [ "234657" ] },
  626. 237125: { missedTextUpdates: [ "237192" ] },
  627. 237665: { imageQuest: true, ignoreTextPosts: [ "" ] },
  628. 238281: { ignoreTextPosts: [ "TK" ] },
  629. 238993: { missedTextUpdates: [ "239018", "239028", "239094" ] },
  630. 240824: { imageQuest: false},
  631. 241467: { missedTextUpdates: [ "241709" ] },
  632. 242200: { missedTextUpdates: [ "246465", "246473", "246513" ] },
  633. 242657: { missedAuthors: [ "2563d4" ] },
  634. 244225: { missedTextUpdates: [ "245099", "245195", "245201" ] },
  635. 244557: { missedTextUpdates: [ "244561" ], ignoreTextPosts: [ "" ] },
  636. 244830: { missedAuthors: [ "e33093" ] },
  637. 247108: { ignoreTextPosts: [ "Bahu" ], wrongImageUpdates: [ "258883", "265446" ] },
  638. 247714: { missedTextUpdates: [ "247852" ] },
  639. 248067: { ignoreTextPosts: [ "" ] },
  640. 248856: { ignoreTextPosts: [ "" ] },
  641. 248880: { imageQuest: true, ignoreTextPosts: [ "", "!qkgg.NzvRY", "!!EyA2IwLwVl", "!I10GFLsZCw", "!k6uRjGDgAQ", "Seven01a19" ] },
  642. 251909: { missedTextUpdates: [ "255400" ] },
  643. 252195: { missedTextUpdates: [ "260890" ] },
  644. 252944: { missedAuthors: [ "Rizzie" ], ignoreTextPosts: [ "", "!!EyA2IwLwVl", "Seven01a19" ] },
  645. 256339: { missedTextUpdates: [ "256359", "256379", "256404", "256440" ] },
  646. 257726: { missedAuthors: [ "917cac" ] },
  647. 258304: { missedTextUpdates: [ "269087" ] },
  648. 261572: { imageQuest: false},
  649. 261837: { missedAuthors: [ "14149d" ] },
  650. 262128: { missedTextUpdates: [ "262166", "262219", "262455", "262500" ] },
  651. 262574: { missedAuthors: [ "b7798b", "0b5a64", "687829", "446f39", "cc1ccd", "9d3d72", "72d5e4", "932db9", "4d7cb4", "9f327a", "940ab2", "a660d0" ], ignoreTextPosts: [ "" ] },
  652. 263831: { imageQuest: false, wrongTextUpdates: [ "264063", "264716", "265111", "268733", "269012", "270598", "271254", "271852", "271855", "274776", "275128", "280425", "280812", "282417", "284354", "291231", "300074", "305150" ] },
  653. 265656: { ignoreTextPosts: [ "Glaive17" ] },
  654. 266542: { missedAuthors: [ "MidKnight", "c2c011", "f5e4b4", "e973f4", "6547ec" ], ignoreTextPosts: [ "", "!TEEDashxDA", "Not Cirr", "Ñ" ] },
  655. 267348: { ignoreTextPosts: [ "" ] },
  656. 269735: { ignoreTextPosts: [ "---" ] },
  657. 270556: { ignoreTextPosts: [ "Bahu" ], wrongImageUpdates: [ "276022" ] },
  658. 273047: { missedAuthors: [ "db463d", "16f0be", "77df62", "b6733e", "d171a3", "3a95e1", "21d450" ] },
  659. 274088: { missedAuthors: [ "4b0cf3" ], missedTextUpdates: [ "294418" ], ignoreTextPosts: [ "" ] },
  660. 274466: { missedAuthors: [ "c9efe3" ] },
  661. 276562: { missedTextUpdates: [ "277108" ] },
  662. 277371: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  663. 278168: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  664. 280381: { ignoreTextPosts: [ "!7BHo7QtR6I" ] },
  665. 280985: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  666. 283246: { imageQuest: false},
  667. 285210: { ignoreTextPosts: [ "", "Weaver" ] },
  668. 287296: { ignoreTextPosts: [ "", "Asplosionz" ] },
  669. 287815: { missedAuthors: [ "Ñ" ] },
  670. 288346: { missedAuthors: [ "383006", "bf1e7e" ], ignoreTextPosts: [ "383006", "bf1e7e" ] },
  671. 289254: { imageQuest: false},
  672. 292033: { wrongTextUpdates: [ "295088" ] },
  673. 293532: { ignoreTextPosts: [ "" ] },
  674. 294351: { ignoreTextPosts: [ "Weaver" ] },
  675. 295374: { ignoreTextPosts: [ "TK" ] },
  676. 295832: { missedAuthors: [ "ac22cd", "7afbc4", "6f11ff" ], missedTextUpdates: [ "313940" ] },
  677. 295949: { missedTextUpdates: [ "296256", "297926", "298549" ] },
  678. 298133: { missedTextUpdates: [ "298187" ] },
  679. 298860: { imageQuest: true, missedTextUpdates: [ "298871", "298877", "298880", "298908" ] },
  680. 299352: { imageQuest: true, missedTextUpdates: [ "299375", "299627", "303689" ] },
  681. 300694: { ignoreTextPosts: [ "TK" ] },
  682. 300751: { missedTextUpdates: [ "316287" ] },
  683. 303859: { ignoreTextPosts: [ "" ] },
  684. 308257: { missedTextUpdates: [ "314653" ] },
  685. 309753: { missedTextUpdates: [ "309864", "309963", "310292", "310944", "310987", "311202", "311219", "311548" ] },
  686. 310586: { missedTextUpdates: [ "310945", "312747", "313144" ] },
  687. 311021: { missedAuthors: [ "049dfa", "f2a6f9" ] },
  688. 312418: { missedTextUpdates: [ "312786", "312790", "312792", "312984", "313185" ] },
  689. 314825: { ignoreTextPosts: [ "TK" ] },
  690. 314940: { missedTextUpdates: [ "314986", "315198", "329923" ] },
  691. 318478: { ignoreTextPosts: [ "Toxoglossa" ] },
  692. 319491: { ignoreTextPosts: [ "Bahu" ] },
  693. 323481: { missedTextUpdates: [ "323843", "324125", "324574" ] },
  694. 323589: { missedTextUpdates: [ "329499" ] },
  695. 327468: { missedTextUpdates: [ "327480", "337008" ] },
  696. 337661: { ignoreTextPosts: [ "", "hisgooddog" ] },
  697. 338579: { ignoreTextPosts: [ "", "Zealo8", "Ñ" ] },
  698. 343078: { wrongImageUpdates: [ "343219" ] },
  699. 343668: { missedTextUpdates: [ "343671" ] },
  700. 348635: { ignoreTextPosts: [ "" ] },
  701. 351064: { missedTextUpdates: [ "351634", "353263", "355326", "356289" ] },
  702. 351264: { missedTextUpdates: [ "353077" ] },
  703. 354201: { imageQuest: true, missedTextUpdates: [ "354340" ] },
  704. 355404: { ignoreTextPosts: [ "Bahu" ] },
  705. 356715: { missedTextUpdates: [ "356722" ] },
  706. 357723: { missedAuthors: [ "7bad01" ], ignoreTextPosts: [ "", "SoqWizard" ] },
  707. 359879: { imageQuest: false},
  708. 359931: { missedAuthors: [ "Dasaki", "Rynh", "Kinasa", "178c80" ], ignoreTextPosts: [ "", "Gnoll", "Lost Planet", "Dasaki", "Slinkoboy" ] },
  709. 360617: { missedAuthors: [ "7a7217" ] },
  710. 363529: { imageQuest: true, ignoreTextPosts: [ "Tenyoken" ] },
  711. 365082: { missedTextUpdates: [ "381411", "382388" ] },
  712. 366944: { missedTextUpdates: [ "367897" ] },
  713. 367145: { wrongTextUpdates: [ "367887" ] },
  714. 367824: { missedTextUpdates: [ "367841", "367858", "367948" ] },
  715. 375293: { ignoreTextPosts: [ "Bahu" ] },
  716. 382864: { ignoreTextPosts: [ "FlynnMerk" ] },
  717. 387602: { ignoreTextPosts: [ "!a1..dIzWW2" ], wrongImageUpdates: [ "390207", "392018", "394748" ] },
  718. 388264: { ignoreTextPosts: [ "" ] },
  719. 392034: { missedAuthors: [ "046f13" ] },
  720. 392868: { missedAuthors: [ "e1359e" ] },
  721. 393082: { ignoreTextPosts: [ "" ] },
  722. 395700: { missedTextUpdates: [ "395701", "395758" ] },
  723. 395817: { ignoreTextPosts: [ "" ] },
  724. 397819: { ignoreTextPosts: [ "Bahu", "K-Dogg" ], wrongImageUpdates: [ "398064" ] },
  725. 400842: { missedAuthors: [ "b0d466" ], ignoreTextPosts: [ "", "!a1..dIzWW2" ], wrongImageUpdates: [ "412172", "412197" ] },
  726. 403418: { missedAuthors: [ "02cbc6" ] },
  727. 404177: { missedTextUpdates: [ "404633" ] },
  728. 409356: { missedTextUpdates: [ "480664", "485493" ], wrongTextUpdates: [ "492824" ] },
  729. 410618: { ignoreTextPosts: [ "kathryn" ], wrongImageUpdates: [ "417836" ] },
  730. 412463: { ignoreTextPosts: [ "" ] },
  731. 413494: { ignoreTextPosts: [ "Bahu" ] },
  732. 420600: { imageQuest: false},
  733. 421477: { imageQuest: false},
  734. 422052: { missedAuthors: [ "!a1..dIzWW2" ] },
  735. 422087: { ignoreTextPosts: [ "Caz" ] },
  736. 422856: { ignoreTextPosts: [ "", "???" ] },
  737. 424198: { missedAuthors: [ "067a04" ], ignoreTextPosts: [ "!a1..dIzWW2" ] },
  738. 425677: { missedTextUpdates: [ "425893", "426741", "431953" ] },
  739. 426019: { ignoreTextPosts: [ "Taskuhecate" ] },
  740. 427135: { ignoreTextPosts: [ "!7BHo7QtR6I" ] },
  741. 427676: { ignoreTextPosts: [ "FRACTAL" ] },
  742. 428027: { ignoreTextPosts: [ "notrottel", "Bahu", "!a1..dIzWW2", "Trout", "Larro", "", "cuoqet" ], wrongImageUpdates: [ "428285", "498295" ] },
  743. 430036: { missedTextUpdates: [ "430062", "430182", "430416" ], ignoreTextPosts: [ "" ] },
  744. 431445: { imageQuest: false, missedAuthors: [ "efbb86" ] },
  745. 435947: { missedTextUpdates: [ "436059" ] },
  746. 437675: { wrongTextUpdates: [ "445770", "449255", "480401" ] },
  747. 437768: { missedTextUpdates: [ "446536" ] },
  748. 438515: { ignoreTextPosts: [ "TK" ] },
  749. 438670: { ignoreTextPosts: [ "" ] },
  750. 441226: { missedAuthors: [ "6a1ec2", "99090a", "7f2d33" ], wrongImageUpdates: [ "441260" ] },
  751. 441745: { missedTextUpdates: [ "443831" ] },
  752. 447830: { imageQuest: false, missedAuthors: [ "fc985a", "f8b208" ], wrongTextUpdates: [ "448476", "450379", "452161" ] },
  753. 448900: { missedAuthors: [ "0c2256" ] },
  754. 449505: { wrongTextUpdates: [ "450499" ] },
  755. 450563: { missedAuthors: [ "!!AwZwHkBGWx", "Oregano" ], ignoreTextPosts: [ "", "chirps", "!!AwZwHkBGWx", "!!AwZwHkBGWx", "Ham" ] },
  756. 452871: { missedAuthors: [ "General Q. Waterbuffalo", "!cZFAmericA" ], missedTextUpdates: [ "456083" ] },
  757. 453480: { ignoreTextPosts: [ "TK" ], wrongImageUpdates: [ "474233" ] },
  758. 453978: { missedTextUpdates: [ "453986" ] },
  759. 454256: { missedTextUpdates: [ "474914", "474957" ] },
  760. 456185: { ignoreTextPosts: [ "TK" ], wrongTextUpdates: [ "472446" ], wrongImageUpdates: [ "592622" ] },
  761. 456798: { missedTextUpdates: [ "516303" ] },
  762. 458432: { missedAuthors: [ "259cce", "34cbef" ] },
  763. 463595: { missedTextUpdates: [ "463711", "465024", "465212", "465633", "467107", "467286" ], wrongTextUpdates: [ "463623" ] },
  764. 464038: { missedAuthors: [ "df885d", "8474cd" ] },
  765. 465919: { missedTextUpdates: [ "465921" ] },
  766. 469321: { missedTextUpdates: [ "469332" ] },
  767. 471304: { missedAuthors: [ "1766db" ] },
  768. 471394: { missedAuthors: [ "Cirr" ] },
  769. 476554: { ignoreTextPosts: [ "Fish is yum" ] },
  770. 478624: { missedAuthors: [ "88c9b2" ] },
  771. 479712: { ignoreTextPosts: [ "" ] },
  772. 481277: { missedTextUpdates: [ "481301", "482210" ], ignoreTextPosts: [ "Santova" ] },
  773. 481491: { missedTextUpdates: [ "481543", "481575", "484069" ], ignoreTextPosts: [ "Zach Leigh", "Santova", "Outaki Shiba" ] },
  774. 482391: { missedTextUpdates: [ "482501", "482838" ] },
  775. 482629: { missedTextUpdates: [ "484220", "484437" ], ignoreTextPosts: [ "Santova", "Tera Nospis" ] },
  776. 483108: { missedAuthors: [ "2de44c" ], missedTextUpdates: [ "483418", "483658" ], ignoreTextPosts: [ "Santova" ] },
  777. 484423: { missedTextUpdates: [ "484470", "486761", "488602" ], ignoreTextPosts: [ "Tera Nospis", "Zach Leigh" ] },
  778. 484606: { missedTextUpdates: [ "486773" ], ignoreTextPosts: [ "Zach Leigh" ] },
  779. 485964: { missedTextUpdates: [ "489145", "489760" ], ignoreTextPosts: [ "Tera Nospis", "Santova" ] },
  780. 489488: { missedTextUpdates: [ "490389" ] },
  781. 489694: { missedAuthors: [ "2c8bbe", "30a140", "8c4b01", "8fbeb2", "2b7d97", "17675d", "782175", "665fcd", "e91794", "52019c", "8ef0aa", "e493a6", "c847bc" ] },
  782. 489830: { missedAuthors: [ "9ee824", "8817a0", "d81bd3", "704658" ] },
  783. 490689: { ignoreTextPosts: [ "Santova" ] },
  784. 491171: { ignoreTextPosts: [ "Santova", "Zach Leigh", "Zack Leigh", "The Creator" ] },
  785. 491314: { missedTextUpdates: [ "491498" ], ignoreTextPosts: [ "" ] },
  786. 492511: { missedAuthors: [ "???" ] },
  787. 493099: { ignoreTextPosts: [ "Zach Leigh", "Santova" ] },
  788. 494015: { ignoreTextPosts: [ "Coda", "drgruff" ] },
  789. 496561: { ignoreTextPosts: [ "Santova", "DJ LaLonde", "Tera Nospis" ] },
  790. 498874: { ignoreTextPosts: [ "Santova" ] },
  791. 499607: { ignoreTextPosts: [ "Santova", "Tera Nospis" ] },
  792. 499980: { ignoreTextPosts: [ "Santova", "Tera Nospis", "DJ LaLonde" ] },
  793. 500015: { missedTextUpdates: [ "500020", "500029", "500274", "501462", "501464", "501809", "505421" ], ignoreTextPosts: [ "suggestion", "Chelz" ] },
  794. 502751: { ignoreTextPosts: [ "suggestion" ] },
  795. 503053: { missedAuthors: [ "!!WzMJSzZzWx", "Shopkeep", "CAI" ] },
  796. 505072: { missedTextUpdates: [ "565461" ] },
  797. 505569: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  798. 505633: { missedTextUpdates: [ "505694", "529582" ] },
  799. 505796: { ignoreTextPosts: [ "Mister-Saturn" ] },
  800. 506555: { ignoreTextPosts: [ "Tera Nospis", "Santova" ] },
  801. 507761: { ignoreTextPosts: [ "", "Rue" ] },
  802. 508294: { missedAuthors: [ "Lisila" ], missedTextUpdates: [ "508618", "508406" ] },
  803. 509510: { missedTextUpdates: [ "509810", "510805", "510812", "510943", "511042", "512430", "514731", "515963" ] },
  804. 510067: { missedTextUpdates: [ "510081" ] },
  805. 511816: { imageQuest: true, missedAuthors: [ "34cf7d" ], missedTextUpdates: [ "512608" ] },
  806. 512417: { ignoreTextPosts: [ "Uplifted" ] },
  807. 512501: { ignoreTextPosts: [ "" ] },
  808. 512569: { wrongImageUpdates: [ "512810" ] },
  809. 513727: { missedTextUpdates: [ "519251" ], ignoreTextPosts: [ "!mYSM8eo.ng" ] },
  810. 514174: { missedTextUpdates: [ "747164" ] },
  811. 515255: { ignoreTextPosts: [ "" ] },
  812. 516595: { imageQuest: true},
  813. 517144: { ignoreTextPosts: [ "" ] },
  814. 518737: { wrongTextUpdates: [ "521408", "522150", "522185", "522231", "535521" ] },
  815. 518843: { ignoreTextPosts: [ "" ] },
  816. 519463: { imageQuest: false},
  817. 521196: { missedTextUpdates: [ "524608" ] },
  818. 526472: { missedTextUpdates: [ "526524", "559848" ] },
  819. 527296: { ignoreTextPosts: [ "Zealo8" ] },
  820. 527546: { ignoreTextPosts: [ "suggestion" ] },
  821. 527753: { missedAuthors: [ "7672c3", "9d78a6", "cb43c1" ] },
  822. 528891: { ignoreTextPosts: [ "drgruff" ] },
  823. 530940: { missedAuthors: [ "2027bb", "feafa5", "0a3b00" ] },
  824. 533990: { missedTextUpdates: [ "537577" ] },
  825. 534197: { ignoreTextPosts: [ "Stella" ] },
  826. 535302: { ignoreTextPosts: [ "mermaid" ] },
  827. 535783: { ignoreTextPosts: [ "drgruff" ] },
  828. 536268: { missedTextUpdates: [ "536296", "538173" ], ignoreTextPosts: [ "Archivemod" ], wrongImageUpdates: [ "537996" ] },
  829. 537343: { missedTextUpdates: [ "539218" ] },
  830. 537647: { missedTextUpdates: [ "537683" ] },
  831. 537867: { missedAuthors: [ "369097" ] },
  832. 539831: { ignoreTextPosts: [ "" ] },
  833. 540147: { ignoreTextPosts: [ "drgruff" ] },
  834. 541026: { imageQuest: false},
  835. 543428: { missedTextUpdates: [ "545458" ] },
  836. 545071: { missedTextUpdates: [ "545081" ] },
  837. 545791: { ignoreTextPosts: [ "" ] },
  838. 545842: { missedTextUpdates: [ "550972" ] },
  839. 548052: { missedTextUpdates: [ "548172" ], ignoreTextPosts: [ "Lucid" ] },
  840. 548899: { missedTextUpdates: [ "548968", "549003" ] },
  841. 549394: { missedTextUpdates: [ "549403" ] },
  842. 553434: { missedTextUpdates: [ "553610", "553635", "553668", "554166" ] },
  843. 553711: { missedTextUpdates: [ "553722", "553728", "554190" ] },
  844. 553760: { missedTextUpdates: [ "554994", "555829", "556570", "556792", "556803", "556804" ] },
  845. 554694: { missedTextUpdates: [ "557011", "560544" ] },
  846. 556435: { missedAuthors: [ "Azathoth" ], missedTextUpdates: [ "607163" ], wrongTextUpdates: [ "561150" ] },
  847. 557051: { missedTextUpdates: [ "557246", "557260", "557599", "559586" ], wrongTextUpdates: [ "557517" ] },
  848. 557633: { imageQuest: true},
  849. 557854: { missedTextUpdates: [ "557910", "557915", "557972", "558082", "558447", "558501", "561834", "561836", "562289", "632102", "632481", "632509", "632471" ] },
  850. 562193: { ignoreTextPosts: [ "" ] },
  851. 563459: { missedTextUpdates: [ "563582" ] },
  852. 564852: { ignoreTextPosts: [ "Trout" ] },
  853. 564860: { missedTextUpdates: [ "565391" ] },
  854. 565909: { ignoreTextPosts: [ "" ] },
  855. 567119: { missedTextUpdates: [ "573494", "586375" ] },
  856. 567138: { missedAuthors: [ "4cf1b6" ] },
  857. 568248: { missedTextUpdates: [ "569818" ] },
  858. 568370: { ignoreTextPosts: [ "" ] },
  859. 568463: { missedTextUpdates: [ "568470", "568473" ] },
  860. 569225: { missedTextUpdates: [ "569289" ] },
  861. 573815: { wrongTextUpdates: [ "575792" ] },
  862. 578213: { missedTextUpdates: [ "578575" ] },
  863. 581741: { missedTextUpdates: [ "581746" ] },
  864. 582268: { missedTextUpdates: [ "587221" ] },
  865. 585201: { ignoreTextPosts: [ "", "Bahustard", "Siphon" ] },
  866. 586024: { ignoreTextPosts: [ "" ] },
  867. 587086: { missedTextUpdates: [ "587245", "587284", "587443", "587454" ] },
  868. 587562: { ignoreTextPosts: [ "Zealo8" ] },
  869. 588902: { missedTextUpdates: [ "589033" ] },
  870. 589725: { imageQuest: false},
  871. 590502: { ignoreTextPosts: [ "" ], wrongTextUpdates: [ "590506" ] },
  872. 590761: { missedTextUpdates: [ "590799" ], ignoreTextPosts: [ "" ] },
  873. 591527: { missedTextUpdates: [ "591547", "591845" ] },
  874. 592273: { imageQuest: false},
  875. 592625: { wrongTextUpdates: [ "730228" ] },
  876. 593047: { missedTextUpdates: [ "593065", "593067", "593068" ] },
  877. 593899: { ignoreTextPosts: [ "mermaid" ] },
  878. 595081: { ignoreTextPosts: [ "", "VoidWitchery" ] },
  879. 595265: { imageQuest: false, wrongTextUpdates: [ "596676", "596717", "621360", "621452", "621466", "621469", "621503" ] },
  880. 596262: { missedTextUpdates: [ "596291", "596611", "597910", "598043", "598145", "600718", "603311" ] },
  881. 596345: { ignoreTextPosts: [ "mermaid" ] },
  882. 596539: { missedTextUpdates: [ "596960", "596972", "596998", "597414", "614375", "614379", "614407", "616640", "668835", "668844", "668906", "668907", "668937", "668941", "669049", "669050",
  883. "669126", "671651" ], ignoreTextPosts: [ "pugbutt" ] },
  884. 598767: { ignoreTextPosts: [ "FRACTAL" ] },
  885. 602894: { ignoreTextPosts: [ "" ] },
  886. 604604: { missedTextUpdates: [ "605127", "606702" ] },
  887. 609653: { missedTextUpdates: [ "610108", "610137" ] },
  888. 611369: { wrongImageUpdates: [ "620890" ] },
  889. 611997: { missedTextUpdates: [ "612102", "612109" ], wrongTextUpdates: [ "617447" ] },
  890. 613977: { missedTextUpdates: [ "614036" ] },
  891. 615246: { missedTextUpdates: [ "638243", "638245", "638246", "638248" ] },
  892. 615752: { ignoreTextPosts: [ "Uplifted" ] },
  893. 617061: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  894. 617484: { missedTextUpdates: [ "617509", "617830" ] },
  895. 618712: { missedTextUpdates: [ "619097", "619821", "620260" ] },
  896. 620830: { missedAuthors: [ "913f0d" ], ignoreTextPosts: [ "", "Sky-jaws" ] },
  897. 623611: { ignoreTextPosts: [ "!5tTWT1eydY" ] },
  898. 623897: { wrongTextUpdates: [ "625412" ] },
  899. 625364: { missedTextUpdates: [ "635199" ] },
  900. 625814: { missedAuthors: [ "330ce5", "f79974", "53688c", "a19cd5", "defceb" ], missedTextUpdates: [ "625990" ], ignoreTextPosts: [ "" ] },
  901. 627139: { ignoreTextPosts: [ "", "Seal" ] },
  902. 628023: { missedTextUpdates: [ "628323", "629276", "629668" ] },
  903. 628357: { ignoreTextPosts: [ "" ] },
  904. 632345: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  905. 632823: { missedTextUpdates: [ "632860", "633225", "633632", "633649", "633723", "634118" ], ignoreTextPosts: [ "" ] },
  906. 633187: { missedTextUpdates: [ "633407", "633444", "634031", "634192", "634462" ] },
  907. 633487: { missedAuthors: [ "8b8b34", "fe7a48", "20ca72", "668d91" ] },
  908. 634122: { ignoreTextPosts: [ "Apollo" ] },
  909. 639549: { ignoreTextPosts: [ "Apollo" ] },
  910. 641286: { missedTextUpdates: [ "641650" ] },
  911. 642667: { missedTextUpdates: [ "643113" ] },
  912. 642726: { missedTextUpdates: [ "648209", "651723" ] },
  913. 643327: { ignoreTextPosts: [ "" ] },
  914. 644179: { missedTextUpdates: [ "647317" ] },
  915. 645426: { missedTextUpdates: [ "651214", "670665", "671751", "672911", "674718", "684082" ] },
  916. 648109: { missedTextUpdates: [ "711809", "711811" ] },
  917. 648646: { missedTextUpdates: [ "648681" ] },
  918. 651220: { missedTextUpdates: [ "653791" ] },
  919. 651382: { missedAuthors: [ "bbfc3d" ] },
  920. 651540: { missedTextUpdates: [ "651629" ] },
  921. 655158: { ignoreTextPosts: [ "" ] },
  922. 662096: { ignoreTextPosts: [ "" ] },
  923. 662196: { missedAuthors: [ "Penelope" ], ignoreTextPosts: [ "", "Brom", "Wire" ] },
  924. 662452: { ignoreTextPosts: [ "" ] },
  925. 662661: { ignoreTextPosts: [ "" ] },
  926. 663088: { missedAuthors: [ "f68a09", "8177e7" ], ignoreTextPosts: [ "", "!5tTWT1eydY", "Wire", "Brom", "Apollo", "Arhra" ] },
  927. 663996: { missedTextUpdates: [ "673890" ] },
  928. 668009: { missedTextUpdates: [ "668227" ] },
  929. 668216: { imageQuest: false},
  930. 669206: { imageQuest: true, missedAuthors: [ "75347e" ] },
  931. 672060: { missedTextUpdates: [ "673216" ] },
  932. 673444: { ignoreTextPosts: [ "" ] },
  933. 673575: { missedAuthors: [ "a6f913", "3bc92d" ], ignoreTextPosts: [ "!5tTWT1eydY" ] },
  934. 673811: { missedTextUpdates: [ "682275", "687221", "687395", "688995" ], ignoreTextPosts: [ "" ] },
  935. 677271: { missedTextUpdates: [ "677384" ] },
  936. 678114: { imageQuest: false},
  937. 678608: { missedTextUpdates: [ "678789" ] },
  938. 679357: { missedTextUpdates: [ "679359", "679983" ] },
  939. 680125: { ignoreTextPosts: [ "", "BritishHat" ] },
  940. 680206: { missedAuthors: [ "Gnuk" ] },
  941. 681620: { missedAuthors: [ "d9faec" ] },
  942. 683261: { missedAuthors: [ "3/8 MPP, 4/4 MF" ] },
  943. 686590: { imageQuest: false},
  944. 688371: { missedTextUpdates: [ "696249", "696257" ], ignoreTextPosts: [ "", "Chaos", "Ariadne", "Melinoe", "\"F\"ingGenius" ] },
  945. 691136: { missedTextUpdates: [ "697620" ], ignoreTextPosts: [ "" ], wrongImageUpdates: [ "706696" ] },
  946. 691255: { ignoreTextPosts: [ "" ] },
  947. 692093: { missedAuthors: [ "Bergeek" ], ignoreTextPosts: [ "Boxdog" ] },
  948. 692872: { missedTextUpdates: [ "717187" ] },
  949. 693509: { missedAuthors: [ "640f86" ] },
  950. 693648: { missedTextUpdates: [ "694655" ] },
  951. 694230: { ignoreTextPosts: [ "" ] },
  952. 700573: { missedTextUpdates: [ "702352", "720330" ], ignoreTextPosts: [ "" ] },
  953. 701456: { ignoreTextPosts: [ "" ] },
  954. 702865: { ignoreTextPosts: [ "" ] },
  955. 705639: { wrongTextUpdates: [ "794696" ] },
  956. 706303: { missedAuthors: [ "5a8006" ] },
  957. 706439: { missedTextUpdates: [ "714791" ] },
  958. 706938: { ignoreTextPosts: [ "" ] },
  959. 711320: { missedTextUpdates: [ "720646", "724022" ] },
  960. 712179: { missedTextUpdates: [ "712255", "715182" ] },
  961. 712785: { ignoreTextPosts: [ "" ] },
  962. 713042: { missedTextUpdates: [ "713704" ] },
  963. 714130: { imageQuest: true},
  964. 714290: { missedTextUpdates: [ "714307", "714311" ] },
  965. 714858: { ignoreTextPosts: [ "" ] },
  966. 715796: { ignoreTextPosts: [ "" ] },
  967. 717114: { missedTextUpdates: [ "717454", "717628" ] },
  968. 718797: { missedAuthors: [ "FRACTAL on the go" ] },
  969. 718844: { missedAuthors: [ "kome", "Vik", "Friptag" ], missedTextUpdates: [ "721242" ] },
  970. 719505: { ignoreTextPosts: [ "" ] },
  971. 719579: { imageQuest: false},
  972. 722585: { wrongTextUpdates: [ "724938" ] },
  973. 726944: { ignoreTextPosts: [ "" ] },
  974. 727356: { ignoreTextPosts: [ "" ] },
  975. 727581: { missedTextUpdates: [ "728169" ] },
  976. 727677: { ignoreTextPosts: [ "Melinoe" ] },
  977. 728411: { missedTextUpdates: [ "728928" ] },
  978. 730993: { missedTextUpdates: [ "731061" ] },
  979. 732214: { imageQuest: true, wrongTextUpdates: [ "732277" ] },
  980. 734610: { ignoreTextPosts: [ "D3w" ] },
  981. 736484: { ignoreTextPosts: [ "Roman" ], wrongImageUpdates: [ "750212", "750213", "750214" ] },
  982. 741609: { missedTextUpdates: [ "754524" ] },
  983. 743976: { ignoreTextPosts: [ "", "Typo" ] },
  984. 745694: { ignoreTextPosts: [ "Crunchysaurus" ] },
  985. 750281: { ignoreTextPosts: [ "Autozero" ] },
  986. 752572: { missedTextUpdates: [ "752651", "752802", "767190" ] },
  987. 754415: { missedAuthors: [ "Apollo", "riotmode", "!0iuTMXQYY." ], ignoreTextPosts: [ "", "!5tTWT1eydY", "!0iuTMXQYY.", "Indonesian Gentleman" ] },
  988. 755378: { missedAuthors: [ "!Ykw7p6s1S." ] },
  989. 758668: { ignoreTextPosts: [ "LD" ] },
  990. 767346: { ignoreTextPosts: [ "" ] },
  991. 768858: { ignoreTextPosts: [ "LD" ] },
  992. 774368: { missedTextUpdates: [ "774500" ] },
  993. 774930: { missedTextUpdates: [ "794040" ] },
  994. 778045: { missedTextUpdates: [ "778427", "779363" ] },
  995. 779564: { ignoreTextPosts: [ "" ] },
  996. 784068: { wrongTextUpdates: [ "785618" ] },
  997. 785044: { wrongTextUpdates: [ "801329" ] },
  998. 789976: { missedTextUpdates: [ "790596", "793934", "800875", "832472" ] },
  999. 794320: { wrongTextUpdates: [ "795183" ] },
  1000. 798380: { missedTextUpdates: [ "799784", "800444", "800774", "800817", "801212" ] },
  1001. 799546: { missedTextUpdates: [ "801103", "802351", "802753" ] },
  1002. 799612: { missedTextUpdates: [ "799968", "801579" ] },
  1003. 800605: { missedAuthors: [ "Boris Calija", "3373e2", "2016eb", "a80028" ], ignoreTextPosts: [ "", "Boris Calija" ] },
  1004. 802411: { missedTextUpdates: [ "805002" ] },
  1005. 807972: { wrongTextUpdates: [ "811969" ] },
  1006. 809039: { wrongImageUpdates: [ "817508", "817511" ] },
  1007. 811957: { ignoreTextPosts: [ "via Discord" ] },
  1008. 814448: { missedTextUpdates: [ "817938" ] },
  1009. 817541: { missedAuthors: [ "Raptie" ] },
  1010. 822552: { imageQuest: false},
  1011. 823831: { missedAuthors: [ "Retro-LOPIS" ] },
  1012. 827264: { ignoreTextPosts: [ "LD", "DogFace" ] },
  1013. 830006: { missedAuthors: [ "Amaranth" ] },
  1014. 835062: { ignoreTextPosts: [ "Curves" ] },
  1015. 835750: { missedTextUpdates: [ "836870" ] },
  1016. 836521: { wrongTextUpdates: [ "848748" ] },
  1017. 837514: { ignoreTextPosts: [ "LD" ] },
  1018. 839906: { missedTextUpdates: [ "845724" ] },
  1019. 840029: { missedTextUpdates: [ "840044", "840543" ] },
  1020. 841851: { ignoreTextPosts: [ "Serpens", "Joy" ] },
  1021. 842392: { missedTextUpdates: [ "842434", "842504", "842544" ] },
  1022. 844537: { missedTextUpdates: [ "847326" ] },
  1023. 848887: { imageQuest: true, wrongTextUpdates: [ "851878" ] },
  1024. 854088: { missedTextUpdates: [ "860219" ], ignoreTextPosts: [ "Ursula" ] },
  1025. 854203: { ignoreTextPosts: [ "Zenthis" ] },
  1026. 857294: { wrongTextUpdates: [ "857818" ] },
  1027. 858913: { imageQuest: false},
  1028. 863241: { missedTextUpdates: [ "863519" ] },
  1029. 865754: { missedTextUpdates: [ "875371" ], ignoreTextPosts: [ "???" ] },
  1030. 869242: { ignoreTextPosts: [ "" ] },
  1031. 871667: { missedTextUpdates: [ "884575" ] },
  1032. 876808: { imageQuest: false},
  1033. 879456: { missedTextUpdates: [ "881847" ] },
  1034. 881097: { missedTextUpdates: [ "881292", "882339" ] },
  1035. 881374: { ignoreTextPosts: [ "LD" ] },
  1036. 885481: { imageQuest: false, wrongTextUpdates: [ "886892" ] },
  1037. 890023: { missedAuthors: [ "595acb" ] },
  1038. 897318: { missedTextUpdates: [ "897321", "897624" ] },
  1039. 897846: { missedTextUpdates: [ "897854", "897866" ] },
  1040. 898917: { missedAuthors: [ "Cee (mobile)" ] },
  1041. 900852: { missedTextUpdates: [ "900864" ] },
  1042. 904316: { missedTextUpdates: [ "904356", "904491" ] },
  1043. 907309: { missedTextUpdates: [ "907310" ] },
  1044. 913803: { ignoreTextPosts: [ "Typo" ] },
  1045. 915945: { missedTextUpdates: [ "916021" ] },
  1046. 917513: { missedTextUpdates: [ "917515" ] },
  1047. 918806: { missedTextUpdates: [ "935207" ] },
  1048. 921083: { ignoreTextPosts: [ "LawyerDog" ] },
  1049. 923174: { ignoreTextPosts: [ "Marn", "MarnMobile" ] },
  1050. 924317: { ignoreTextPosts: [ "" ] },
  1051. 926927: { missedTextUpdates: [ "928194" ] },
  1052. 929545: { missedTextUpdates: [ "929634" ] },
  1053. 930854: { missedTextUpdates: [ "932282" ] },
  1054. 934026: { missedTextUpdates: [ "934078", "934817" ] },
  1055. 935464: { missedTextUpdates: [ "935544", "935550", "935552", "935880" ] },
  1056. 939572: { missedTextUpdates: [ "940402" ] },
  1057. 1000012: { missedAuthors: [ "Happiness" ] }
  1058. };
  1059. }
  1060. return this.allFixes;
  1061. }
  1062. }
  1063.  
  1064. //QuestReader class
  1065. //Input: non
  1066. //Output: none
  1067. //Usage: var results = new UpdateAnalyzer().processQuest(document);
  1068. class QuestReader {
  1069. constructor() {
  1070. this.updates = [];
  1071. this.sequences = [];
  1072. this.onSettingsChanged = null;
  1073.  
  1074. var defaultSettings = this.getDefaultSettings();
  1075. this.currentUpdateIndex = defaultSettings.currentUpdateIndex;
  1076. this.viewMode = defaultSettings.viewMode;
  1077. this.showSuggestions = defaultSettings.showSuggestions;
  1078. this.showAuthorComments = defaultSettings.showAuthorComments;
  1079. this.expandImages = defaultSettings.expandImages;
  1080. this.replyAtBottom = defaultSettings.replyAtBottom;
  1081. }
  1082.  
  1083. init(settings) {
  1084. var updateAnalyzer = new UpdateAnalyzer();
  1085. var postTypes = updateAnalyzer.analyzeQuest(document); //run UpdateAnalyzer to determine which posts are updates and what not
  1086. this.threadID = updateAnalyzer.threadID;
  1087. this.updates = this.getUpdatePostGroups(postTypes); //organize posts into groups; 1 update per group
  1088. this.sequences = this.getUpdateSequences(); //a list of unique update sequences; not really needed...
  1089. this.insertControls(); //insert html elements for controls
  1090. this.insertStyling(); //insert html elements for styling
  1091. this.enableHotkeys(); //insert events to capture keys
  1092. this.monitorUrlHashChanges(); //insert event to detect #hash change in the url; when clicking on a post link, automatically change the page to the one that has the post
  1093. this.setSettings(this.validateSettings(settings)); //load settings
  1094. this.refresh(true); //hide all posts and show only the relevant ones; enable/disable/update controls
  1095. }
  1096.  
  1097. getUpdatePostGroups(postTypes) {
  1098. var updatePostGroups = [];
  1099. var currentPostGroup = { updatePostID: 0, suggestions: [], authorComments: [] };
  1100. var postTypesArray = [...postTypes];
  1101. for (let i = postTypesArray.length - 1; i >= 0; i--) {
  1102. if (postTypesArray[i][1] == PostType.UPDATE) {
  1103. currentPostGroup.updatePostID = postTypesArray[i][0];
  1104. updatePostGroups.unshift(currentPostGroup);
  1105. currentPostGroup = { updatePostID: 0, suggestions: [], authorComments: [] };
  1106. }
  1107. else if (postTypesArray[i][1] == PostType.AUTHORCOMMENT) {
  1108. currentPostGroup.authorComments.unshift(postTypesArray[i][0]);
  1109. }
  1110. else {
  1111. currentPostGroup.suggestions.unshift(postTypesArray[i][0]);
  1112. }
  1113. }
  1114. var currentUpdateSequence = [];
  1115. updatePostGroups.forEach(postGroup => {
  1116. currentUpdateSequence.push(postGroup);
  1117. postGroup.sequence = currentUpdateSequence;
  1118. if (postGroup.suggestions.length > 0) {
  1119. currentUpdateSequence = [];
  1120. }
  1121. });
  1122. return updatePostGroups;
  1123. }
  1124.  
  1125. getUpdateSequences() {
  1126. var sequences = [];
  1127. this.updates.forEach(update => {
  1128. if (update.sequence !== sequences[sequences.length - 1]) {
  1129. sequences.push(update.sequence);
  1130. }
  1131. });
  1132. return sequences;
  1133. }
  1134.  
  1135. currentUpdate() {
  1136. return this.updates[this.currentUpdateIndex];
  1137. }
  1138.  
  1139. firstUpdate() {
  1140. return this.updates[0];
  1141. }
  1142.  
  1143. lastUpdate() {
  1144. return this.updates[this.updates.length - 1];
  1145. }
  1146.  
  1147. refresh(checkHash) {
  1148. this.hideAll();
  1149. this.showCurrentUpdates();
  1150. this.updateControls();
  1151.  
  1152. var scrollToPostID = this.currentUpdate().updatePostID;
  1153. if (checkHash && document.defaultView.location.hash) {
  1154. scrollToPostID = document.defaultView.location.hash.replace("#", "");
  1155. this.currentUpdateIndex = this.findUpdate(scrollToPostID);
  1156. }
  1157. var el = scrollToPostID == this.threadID ? document.querySelector("body > form") : document.getElementById(`reply${scrollToPostID}`);
  1158. var scrollOptions = { behavior: "smooth", block: this.viewMode == "all" ? "start" : "nearest" };
  1159. setTimeout(() => { el.scrollIntoView(scrollOptions); }, 0);
  1160. }
  1161.  
  1162. hideAll() {
  1163. document.getElementsByName(this.threadID.toString())[0].parentElement.parentElement.childElements().forEach(opPostChildEl => {
  1164. if (opPostChildEl.className === "postwidth" || opPostChildEl.nodeName === "BLOCKQUOTE" || opPostChildEl.nodeName === "A" || opPostChildEl.className === "de-refmap") {
  1165. opPostChildEl.style.display = "none";
  1166. }
  1167. });
  1168. document.querySelectorAll("form table").forEach(tableEl => {
  1169. if (!tableEl.className) {
  1170. tableEl.style.display = "none";
  1171. }
  1172. });
  1173. }
  1174.  
  1175. findUpdate(postID) {
  1176. for (var i = 0; i < this.updates.length; i++) {
  1177. if (this.updates[i].updatePostID == postID || this.updates[i].suggestions.indexOf(postID) != -1 || this.updates[i].authorComments.indexOf(postID) != -1) {
  1178. return i;
  1179. }
  1180. }
  1181. }
  1182.  
  1183. showCurrentUpdates() {
  1184. if (this.viewMode == "sequence") {
  1185. this.currentUpdate().sequence.forEach(update => this.showUpdate(update));
  1186. }
  1187. else if (this.viewMode == "single") {
  1188. this.showUpdate(this.currentUpdate());
  1189. }
  1190. else {
  1191. this.updates.forEach(update => this.showUpdate(update));
  1192. }
  1193. }
  1194.  
  1195. showUpdate(update) {
  1196. this.showPost(update.updatePostID);
  1197. if (this.showSuggestions) {
  1198. update.suggestions.forEach(postID => this.showPost(postID));
  1199. }
  1200. if (this.showAuthorComments) {
  1201. update.authorComments.forEach(postID => this.showPost(postID));
  1202. }
  1203. }
  1204.  
  1205. showPost(postID) {
  1206. if (postID == this.threadID) {
  1207. document.getElementsByName(postID.toString())[0].parentElement.parentElement.childElements().forEach(node => {
  1208. if (node.className === "postwidth" || node.nodeName === "BLOCKQUOTE") {
  1209. node.style.display = "";
  1210. }
  1211. });
  1212. }
  1213. else {
  1214. var node = document.getElementById(`reply${postID}`).parentElement.parentElement.parentElement;
  1215. node.style.display = "";
  1216. }
  1217. if (this.expandImages) {
  1218. this.expandImage(postID);
  1219. }
  1220. }
  1221.  
  1222. showFirst() {
  1223. this.currentUpdateIndex = 0;
  1224. this.refresh();
  1225. this.settingsChanged();
  1226. }
  1227.  
  1228. showLast() {
  1229. this.currentUpdateIndex = this.updates.length - 1;
  1230. this.refresh();
  1231. this.settingsChanged();
  1232. }
  1233.  
  1234. showNext() {
  1235. if (this.viewMode == "sequence") {
  1236. var currentUpdateSequence = this.currentUpdate().sequence;
  1237. while (this.currentUpdateIndex < this.updates.length - 1 && this.updates[this.currentUpdateIndex].sequence == currentUpdateSequence) {
  1238. this.currentUpdateIndex++;
  1239. }
  1240. if (this.currentUpdate().sequence == this.lastUpdate().sequence) { // in case the user switches to single update view and this is the last sequence, he should be met with the last update
  1241. this.currentUpdateIndex = this.updates.length - 1;
  1242. }
  1243. }
  1244. else if (this.currentUpdateIndex < this.updates.length - 1) {
  1245. this.currentUpdateIndex++;
  1246. }
  1247. this.refresh();
  1248. this.settingsChanged();
  1249. }
  1250.  
  1251. showPrevious() {
  1252. if (this.viewMode == "sequence") {
  1253. var currentUpdateSequence = this.currentUpdate().sequence;
  1254. while (this.currentUpdateIndex > 0 && this.updates[this.currentUpdateIndex].sequence == currentUpdateSequence) {
  1255. this.currentUpdateIndex--;
  1256. }
  1257. if (this.currentUpdate().sequence == this.firstUpdate().sequence) { // in case the user switches to single update view and this is the first sequence, he should be met with the first update
  1258. this.currentUpdateIndex = 0;
  1259. }
  1260. }
  1261. else if (this.currentUpdateIndex > 0) {
  1262. this.currentUpdateIndex--;
  1263. }
  1264. this.refresh();
  1265. this.settingsChanged();
  1266. }
  1267.  
  1268. setSettings(settings) {
  1269. if (settings) {
  1270. for(var settingName in settings) {
  1271. this[settingName] = settings[settingName];
  1272. }
  1273. }
  1274. }
  1275.  
  1276. validateSettings(settings) {
  1277. if (!settings) {
  1278. return settings;
  1279. }
  1280. if (settings.currentUpdateIndex < 0) settings.currentUpdateIndex = 0;
  1281. if (settings.currentUpdateIndex >= this.updates.length) settings.currentUpdateIndex = this.updates.length - 1;
  1282. return settings;
  1283. }
  1284.  
  1285. settingsChanged() {
  1286. if (this.onSettingsChanged) {
  1287. var settings = {
  1288. currentUpdateIndex: this.currentUpdateIndex,
  1289. viewMode: this.viewMode,
  1290. showSuggestions: this.showSuggestions,
  1291. showAuthorComments: this.showAuthorComments,
  1292. expandImages: this.expandImages,
  1293. replyAtBottom: this.replyAtBottom,
  1294. }
  1295. this.onSettingsChanged(settings);
  1296. }
  1297. }
  1298.  
  1299. getDefaultSettings() {
  1300. return {
  1301. currentUpdateIndex: 0,
  1302. viewMode: "sequence",
  1303. showSuggestions: true,
  1304. showAuthorComments: true,
  1305. expandImages: false,
  1306. replyAtBottom: true,
  1307. };
  1308. }
  1309.  
  1310. toggleSettingsControls() {
  1311. event.preventDefault(); //prevent scrolling to the top when clicking the link
  1312. var settingsEl = document.querySelector(".qrSettingsControls");
  1313. settingsEl.classList.toggle("collapsed");
  1314. var label = event.target;
  1315. label.text = settingsEl.classList.contains("collapsed") ? "Settings" : "Hide Settings";
  1316. }
  1317.  
  1318. updateSettings() {
  1319. this.viewMode = document.getElementById("qrShowUpdatesDropdown").value;
  1320. this.showSuggestions = document.getElementById("qrShowSuggestionsCheckbox").checked === true;
  1321. this.showAuthorComments = document.getElementById("qrShowAuthorCommentsCheckbox").checked === true;
  1322. this.expandImages = document.getElementById("qrExpandImagesCheckbox").checked === true;
  1323. this.replyAtBottom = document.getElementById("qrReplyFormAtBottom").checked === true;
  1324. this.refresh();
  1325. this.settingsChanged();
  1326. }
  1327.  
  1328. updateControls() {
  1329. var leftDisabled = true;
  1330. var rightDisabled = true;
  1331. var current = 1;
  1332. var last = 1;
  1333. var suggestionsCount;
  1334. var authorCommentsCount;
  1335. if (this.viewMode == "sequence") {
  1336. leftDisabled = this.currentUpdate().sequence == this.firstUpdate().sequence;
  1337. rightDisabled = this.currentUpdate().sequence == this.lastUpdate().sequence;
  1338. current = this.sequences.indexOf(this.currentUpdate().sequence) + 1;
  1339. last = this.sequences.length;
  1340. suggestionsCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].suggestions.length;
  1341. authorCommentsCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].authorComments.length;
  1342. }
  1343. else {
  1344. leftDisabled = this.currentUpdate() == this.firstUpdate();
  1345. rightDisabled = this.currentUpdate() == this.lastUpdate();
  1346. current = this.currentUpdateIndex + 1;
  1347. last = this.updates.length;
  1348. if (this.viewMode == "single") {
  1349. suggestionsCount = this.currentUpdate().suggestions.length;
  1350. authorCommentsCount = this.currentUpdate().authorComments.length;
  1351. }
  1352. else {
  1353. suggestionsCount = this.updates.reduceRight((sum, update) => { sum = sum + update.suggestions.length; return sum; }, 0);
  1354. authorCommentsCount = this.updates.reduceRight((sum, update) => { sum = sum + update.authorComments.length; return sum; }, 0);
  1355. }
  1356. }
  1357. // buttons
  1358. document.querySelectorAll("#qrShowFirstButton, #qrShowPrevButton").forEach(button => { button.disabled = leftDisabled; });
  1359. document.querySelectorAll("#qrShowNextButton, #qrShowLastButton").forEach(button => { button.disabled = rightDisabled; });
  1360. // update info
  1361. document.querySelectorAll("#qrNavPosition").forEach(label => { label.textContent = `${current} / ${last}`; });
  1362. document.querySelectorAll("#qrSuggestionsCount").forEach(label => { label.textContent = `S:${suggestionsCount}`; });
  1363. document.querySelectorAll("#qrAuthorCommentsCount").forEach(label => { label.textContent = ` A:${authorCommentsCount}`; });
  1364. // settings
  1365. document.getElementById("qrShowUpdatesDropdown").value = this.viewMode;
  1366. document.getElementById("qrShowSuggestionsCheckbox").checked = this.showSuggestions;
  1367. document.getElementById("qrShowAuthorCommentsCheckbox").checked = this.showAuthorComments;
  1368. document.getElementById("qrExpandImagesCheckbox").checked = this.expandImages;
  1369. document.getElementById("qrReplyFormAtBottom").checked = this.replyAtBottom;
  1370. // sticky controls when viewing whole thread
  1371. var controlsContainer = document.querySelector(".qrControlsTop");
  1372. controlsContainer.classList.toggle("sticky", this.viewMode == "all");
  1373. // reply form at bottom
  1374. var postarea = document.querySelector(".postarea");
  1375. var replymode = document.querySelector(".replymode");
  1376. var isReplyFormAtTop = (replymode == postarea.previousElementSibling);
  1377. replymode.style.display = "none";
  1378. if (this.replyAtBottom && isReplyFormAtTop) { //move it down
  1379. postarea.remove();
  1380. document.body.insertBefore(postarea, document.querySelectorAll(".navbar")[1])
  1381. controlsContainer.insertAdjacentHTML("beforeBegin", "<hr>");
  1382. }
  1383. else if (!this.replyAtBottom && !isReplyFormAtTop) { //move it up
  1384. postarea.remove();
  1385. replymode.insertAdjacentElement("afterEnd", postarea);
  1386. controlsContainer.previousElementSibling.remove(); //remove <hr>
  1387. }
  1388. }
  1389.  
  1390. insertControls() {
  1391. document.querySelector("body > form").insertAdjacentHTML("beforebegin", this.getTopControlsHtml());
  1392. document.querySelector(".userdelete").insertAdjacentHTML("beforebegin", this.getNavControlsHtml() + "<hr>");
  1393. }
  1394.  
  1395. insertStyling() {
  1396. document.body.insertAdjacentHTML("beforeend", this.getStylingHtml());
  1397. }
  1398.  
  1399. enableHotkeys() {
  1400. document.addEventListener("keydown", (e) => {
  1401. var inputTypes = ['text', 'password', 'number', 'email', 'tel', 'url', 'search', 'date', 'datetime', 'datetime-local', 'time', 'month', 'week']
  1402. if (e.target.tagName === "TEXTAREA" || (e.target.tagName === "INPUT" && inputTypes.indexOf(e.target.type) >= 0)) {
  1403. return; //prevent our keyboard shortcuts when focused on a text input field
  1404. }
  1405. if (e.key == "ArrowRight") {
  1406. this.showNext();
  1407. }
  1408. else if (e.key == "ArrowLeft") {
  1409. this.showPrevious();
  1410. }
  1411. });
  1412. }
  1413.  
  1414. monitorUrlHashChanges() {
  1415. document.defaultView.onhashchange = (e) => {
  1416. if (document.defaultView.location.hash) {
  1417. this.refresh(true);
  1418. }
  1419. }
  1420. }
  1421.  
  1422. expandImage(postID) {
  1423. var el = document.getElementById(`thumb${postID}`);
  1424. if (!el) return;
  1425. var link = el.parentElement.parentElement.querySelector(".filesize > a");
  1426. var img = el.children[0];
  1427. if (img.src !== link.href) {
  1428. link.click();
  1429. }
  1430. }
  1431.  
  1432. getTopControlsHtml() {
  1433. return `
  1434. <div class="qrControlsTop">
  1435. ${this.getSettingsControlsHtml()}
  1436. <label class="qrSettingsLabel">[<a href="#" id="qrShowSettings" onclick="QR.toggleSettingsControls()">Settings</a>]</label>
  1437. ${this.getNavControlsHtml()}
  1438. <hr>
  1439. </div>
  1440. `;
  1441. }
  1442.  
  1443. getSettingsControlsHtml() {
  1444. return `
  1445. <span class="qrSettingsControls collapsed">
  1446. <span>
  1447. <span>
  1448. <div>Viewing mode:</div>
  1449. <div>Show suggestions:</div>
  1450. <div>Show author comments:</div>
  1451. <div>Keyboard shortcuts:</div>
  1452. </span>
  1453. <span>
  1454. <div>
  1455. <select id="qrShowUpdatesDropdown" class="qrSettingsControl" onchange="QR.updateSettings()">
  1456. <option value="all">Whole thread</option>
  1457. <option value="single">Paged per update</option>
  1458. <option value="sequence">Paged per update sequence</option>
  1459. </select>
  1460. </div>
  1461. <div><input type="checkbox" id="qrShowSuggestionsCheckbox" class="qrSettingsControl" onclick="QR.updateSettings()"></div>
  1462. <div><input type="checkbox" id="qrShowAuthorCommentsCheckbox" class="qrSettingsControl" onclick="QR.updateSettings()"></div>
  1463. <div><span id="hotkeysTooltip" class="qrSettingsControl tooltip">?<span class="tooltiptext">Left and Right arrow keys will navigate between updates</span></span></div>
  1464. </span>
  1465. </span>
  1466. <span>
  1467. <span>
  1468. <div>Expand images:</div>
  1469. <div>Reply form at bottom:</div>
  1470. </span>
  1471. <span>
  1472. <div><input type="checkbox" id="qrExpandImagesCheckbox" class="qrSettingsControl" onclick="QR.updateSettings()"></div>
  1473. <div><input type="checkbox" id="qrReplyFormAtBottom" class="qrSettingsControl" onclick="QR.updateSettings()"></div>
  1474. </span>
  1475. </span>
  1476. </span>
  1477. `;
  1478. }
  1479. getNavControlsHtml() {
  1480. return `
  1481. <div class="qrNavControls">
  1482. <span class="qrNavControl"><button class="qrNavButton" id="qrShowFirstButton" type="button" onclick="QR.showFirst()">First</button></span>
  1483. <span class="qrNavControl"><button class="qrNavButton" id="qrShowPrevButton" type="button" onclick="QR.showPrevious()">Prev</button></span>
  1484. <label id="qrNavPosition" title="Index of the currently shown update slash the total number of updates.">0 / 0</label>
  1485. <span class="qrNavControl"><button class="qrNavButton" id="qrShowNextButton" type="button" onclick="QR.showNext()">Next</button></span>
  1486. <span class="qrNavControl"><button class="qrNavButton" id="qrShowLastButton" type="button" onclick="QR.showLast()">Last</button></span>
  1487. <div class="qrUpdateInfo"><label id="qrSuggestionsCount" title="Number of suggestion posts this update has.">S: 0</label><label id="qrAuthorCommentsCount" title="Number of author comment posts this update has.">A: 0</label></div>
  1488. </div>
  1489. `;
  1490. }
  1491. getStylingHtml() {
  1492. return `
  1493. <style>
  1494. #qrShowFirstButton, #qrShowLastButton { width: 50px; }
  1495. #qrShowPrevButton, #qrShowNextButton { width: 100px; }
  1496. .qrControlsTop { position: relative; text-align: center; }
  1497. .qrSettingsControls { display: flex; justify-content: space-evenly; transition: height 0.3s; height: 80px; overflow: hidden; }
  1498. .qrSettingsControls span { display: inline-block; vertical-align: top; text-align: start; white-space: nowrap; }
  1499. .qrSettingsControls > span > span > div { height: 20px; }
  1500. .qrSettingsControl { margin-left: 4px }
  1501. .qrSettingsLabel { position: absolute; left: 0px; z-index: 1; }
  1502. .qrNavControls { white-space: nowrap; text-align: center; position: relative; }
  1503. .qrUpdateInfo { position: absolute; right: 0px; top: 2px; }
  1504. #hotkeysTooltip { border-bottom: 1px dotted; cursor: pointer; padding: 0px 2px 0px 2px; }
  1505. .collapsed { height: 0px }
  1506. .tooltip { position: relative; display: inline-block; }
  1507. .tooltip:hover .tooltiptext { visibility: visible; }
  1508. .tooltip .tooltiptext { visibility: hidden; width: 225px; text-align: center; padding: 4px; left: 20px;
  1509. position: absolute; border: dotted 1px; z-index: 1; background-color: ${document.defaultView.getComputedStyle(document.body)["background-color"]}; }
  1510. .thumb { width: unset; height: unset;
  1511. max-width: ${100 - (40 / (document.defaultView.innerWidth / 100))}%; max-height: ${100 - (100 / (document.defaultView.innerHeight / 100))}vh; }
  1512. </style>
  1513. `;
  1514. //.sticky { position: sticky; top: 0px; background-color: inherit; }
  1515. }
  1516. }
  1517.  
  1518. if (window.QR) { //sanity check; don't run the script if it already ran
  1519. return;
  1520. }
  1521. if (window.location.href.endsWith("+50.html") || window.location.href.endsWith("+100.html")) {
  1522. return; //also, don't run the script when viewing partial thread
  1523. }
  1524.  
  1525. // for compatibility with certain other extensions, this extension runs last
  1526. setTimeout(() => {
  1527. var timeStart = Date.now();
  1528. // get settings from localStorage
  1529. var threadID = document.postform.replythread.value;
  1530. var lastThreadID = null;
  1531. var settings = window.localStorage.getItem(`qrSettings${threadID}`);
  1532. if (!settings) {
  1533. lastThreadID = window.localStorage.getItem("qrLastThreadID");
  1534. if (lastThreadID) {
  1535. settings = window.localStorage.getItem(`qrSettings${lastThreadID}`);
  1536. if (settings) {
  1537. settings = JSON.parse(settings);
  1538. settings.currentUpdateIndex = 0;
  1539. }
  1540. }
  1541. }
  1542. else {
  1543. settings = JSON.parse(settings);
  1544. }
  1545. window.QR = new QuestReader();
  1546. window.QR.init(settings);
  1547. window.QR.onSettingsChanged = (settings) => {
  1548. if (!lastThreadID || lastThreadID != threadID) {
  1549. window.localStorage.setItem("qrLastThreadID", threadID.toString());
  1550. lastThreadID = threadID;
  1551. }
  1552. window.localStorage.setItem(`qrSettings${threadID}`, JSON.stringify(settings));
  1553. }
  1554. console.log(`Quest Reader run time = ${Date.now() - timeStart}ms`);
  1555. }, 0);