Membean Tracker

A powerful membean helper that uses a variety of tools to answer membean questions.

  1. // ==UserScript==
  2. // @name Membean Tracker
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.0
  5. // @description A powerful membean helper that uses a variety of tools to answer membean questions.
  6. // @author Squidtoon99 (https://squid.pink)
  7. // @include https://membean.com/training_sessions/*/user_state
  8. // @include https://membean.com/mywords/*
  9. // @include https://membean.com/training_sessions/new
  10. // @icon https://www.google.com/s2/favicons?domain=membean.com
  11. // @grant GM_xmlhttpRequest
  12. // @connect squid.pink
  13. // ==/UserScript==
  14. var cache = {};
  15. var apiURL = "https://api.squid.pink";
  16. var q;
  17.  
  18. const plugins = [
  19. autoSelectHTMLPlugin,
  20. questionPlugin,
  21. checkExampleSingleNode,
  22. checkExampleMultiNode,
  23. definitionOnHoverPlugin,
  24. autoNextPlugin,
  25. ];
  26. const persistent_plugins = [storeCorrectAnswerPlugin, autoTypeNewWordPlugin];
  27.  
  28. const non_question_plugins = [
  29. autoAnswerTextInputPlugin,
  30. autoStartNewSessionPlugin,
  31. ];
  32.  
  33. setInterval(() => {
  34. // Checking if there is a new question
  35. var questions = document.getElementsByClassName("question");
  36. if (questions.length > 0) {
  37. var question = questions[0];
  38. var iter;
  39. if (question !== q) {
  40. q = question;
  41. cache.q = question;
  42. iter = plugins; // the plugins that only need to run when the question renders
  43. cache.answer = false;
  44. } else {
  45. iter = persistent_plugins; // the plugins that need to be constantly running
  46. }
  47. for (var z = 0; z < iter.length; z++) {
  48. iter[z]();
  49. }
  50. if (cache.answer == false) {
  51. var choices = document.getElementsByClassName("choice");
  52. setTimeout(() => {
  53. if (
  54. choices === document.getElementsByClassName("choice") &&
  55. document.getElementsByClassName("choice correct").length == 0
  56. ) {
  57. //console.log("autoclicking");
  58. //document.getElementsByClassName("choice")[0].click();
  59. }
  60. }, 500);
  61. }
  62. } else {
  63. for (var x = 0; x < non_question_plugins.length; x++) {
  64. non_question_plugins[x]();
  65. }
  66. }
  67. if (Math.floor(Math.random() * 1000) === 5) {
  68. location.reload();
  69. window.console.log("reloading");
  70. }
  71. }, 500);
  72.  
  73. setInterval(() => {
  74. if (document.getElementsByClassName("take_a_break").length == 1) {
  75. console.log("taking a break");
  76. document.getElementById("Click_me_to_stop").click();
  77. setTimeout(
  78. () =>
  79. (window.location.href = "https://membean.com/training_sessions/new"),
  80. 1000
  81. );
  82. }
  83. }, 1000);
  84.  
  85. function answer(answer_num, reason) {
  86. var o = Object.values(document.getElementsByClassName("choice"))[answer_num];
  87. cache.answer = true;
  88. console.log(`Found correct answer (${answer_num}): [${reason}]`);
  89. o.className = "choice correct";
  90. setTimeout(() => o.click(), 8000); // set this timeout to how long you want to wait (ms) before answering questions
  91. //o.click();
  92. }
  93.  
  94. function autoNextPlugin() {
  95. var c = document.getElementById("next-btn");
  96. if (c) {
  97. setTimeout(() => c.click(), 1000);
  98. }
  99. }
  100.  
  101. function autoSelectHTMLPlugin() {
  102. // This plugin simply will select the choice answer html selector
  103. var c = document.getElementsByClassName("choice answer");
  104. if (c.length > 0) {
  105. c[0].click();
  106. }
  107. }
  108.  
  109. function questionPlugin() {
  110. // This queries the api to see if there is a value stored
  111. var url = `${apiURL}/membean/${encodeURIComponent(q.textContent.trim())}`;
  112. GM_xmlhttpRequest({
  113. method: "GET",
  114. url: url,
  115. headers: {
  116. "Content-Type": "application/json",
  117. },
  118. responseType: "json",
  119. onload: (rspObj) => {
  120. if (rspObj.status == 404) {
  121. // temp autoclicking
  122. var c = document.getElementsByClassName("choice");
  123. return;
  124. } else if (rspObj.status != 200) {
  125. reportAJAX_Error(rspObj);
  126. return;
  127. }
  128. let data = rspObj.response;
  129. console.log(data);
  130. if (data.answer) {
  131. var choices = document.getElementsByClassName("choice");
  132. for (var iter = 0; iter < choices.length; iter++) {
  133. if (choices[iter].textContent.trim() == data.answer.trim()) {
  134. answer(iter, "stored question");
  135. }
  136. }
  137. window.console.log(`[${q}] Answer: ${data.answer[0].answer}`);
  138. } else {
  139. window.console.log("No storage for this question");
  140. }
  141. },
  142. onabort: reportAJAX_Error,
  143. onerror: reportAJAX_Error,
  144. ontimeout: reportAJAX_Error,
  145. });
  146. }
  147.  
  148. function storeCorrectAnswerPlugin() {
  149. // stores the correct answer whenever html is updated
  150. var formatter = document.getElementsByClassName(
  151. "single-column-layout with-image"
  152. );
  153. var correct = document.getElementsByClassName("choice correct");
  154. if (correct.length > 0) {
  155. var e = correct[0];
  156. if (cache.e === e) {
  157. return;
  158. }
  159. cache.e = e;
  160. var data;
  161. if (formatter.length == 1) {
  162. // Images suck why membean why
  163. var img = formatter[0].children[1];
  164. data = { question: img.src, answer: e.textContent };
  165. } else {
  166. // Not an image question just a standard one
  167. data = {
  168. question: cache.q.textContent.trim(),
  169. answer: e.textContent.trim(),
  170. };
  171. }
  172. if (data.question.includes("Try again!")) {
  173. return;
  174. }
  175. GM_xmlhttpRequest({
  176. method: "POST",
  177. url: `${apiURL}/membean/`,
  178. data: JSON.stringify(data),
  179. headers: {
  180. "Content-Type": "application/json",
  181. },
  182. responseType: "json",
  183. onload: (rspObj) => {
  184. if (rspObj.status == 201) {
  185. window.console.log("Stored answer for: " + data.question);
  186. } else {
  187. console.log(rspObj);
  188. }
  189. },
  190. onabort: reportAJAX_Error,
  191. onerror: reportAJAX_Error,
  192. ontimeout: reportAJAX_Error,
  193. });
  194. }
  195. }
  196.  
  197. function checkExampleSingleNode() {
  198. // In case they italize a word we think it's the word that's the answer and then make it look "correct"
  199. var nodes = cache.q.children;
  200. if (nodes.length == 2 && nodes[1].nodeName === "EM") {
  201. var word = nodes[1].textContent;
  202. if (
  203. word[word.length - 1] === "s" &&
  204. !"aeious".split("").includes(word[word.length - 2])
  205. ) {
  206. word = word.slice(0, word.length - 1);
  207. }
  208. if (word.replaceAll("_", "").trim() === "") {
  209. return;
  210. }
  211. console.log(`Question about ${word}`);
  212. GM_xmlhttpRequest({
  213. method: "GET",
  214. url: `https://membean.com/mywords/${word}`,
  215. responseType: "json",
  216. onload: (rspObj) => {
  217. var parser = new DOMParser();
  218. var htmlDoc = parser.parseFromString(rspObj.responseText, "text/html");
  219.  
  220. var canswer = htmlDoc.getElementsByClassName("choice answer")[0];
  221.  
  222. if (!canswer) {
  223. return;
  224. }
  225.  
  226. var choices = document.getElementsByClassName("choice");
  227. var choicesParsed = {};
  228. for (var i = 0; i < choices.length; i++) {
  229. if (choices[i].textContent === canswer.textContent) {
  230. answer(i, "singleNode example");
  231. return;
  232. } else {
  233. choicesParsed[i] = levenshtein(
  234. choices[i].textContent,
  235. canswer.textContent
  236. ); // parsing the most probable answer
  237. }
  238. }
  239. let bestChoice = Object.keys(choicesParsed).reduce((a, b) =>
  240. choicesParsed[a] > choicesParsed[b] ? a : b
  241. );
  242. if (
  243. choicesParsed[bestChoice] >= 10 ||
  244. bestChoice == Object.keys(bestChoice).length - 1
  245. ) {
  246. return; // Not a good enough result
  247. }
  248. answer(bestChoice, "single node best example");
  249. },
  250. onabort: reportAJAX_Error,
  251. onerror: reportAJAX_Error,
  252. ontimeout: reportAJAX_Error,
  253. });
  254. } else if (nodes.length === 3 && nodes[1].nodeName === "EM") {
  255. word =
  256. document.getElementsByClassName("question")[0].children[2].textContent;
  257. console.log("stupid question about roots: " + word);
  258. GM_xmlhttpRequest({
  259. method: "GET",
  260. url: `https://membean.com/mywords/${word}`,
  261. responseType: "json",
  262. onload: (rspObj) => {
  263. var parser = new DOMParser();
  264. var htmlDoc = parser.parseFromString(rspObj.responseText, "text/html");
  265.  
  266. var defData = {};
  267. Object.values(
  268. htmlDoc.getElementById("word-structure").children[1].children[0]
  269. .children[0].children
  270. ).forEach((key) => {
  271. defData[key.children[0].textContent.trim()] =
  272. key.children[2].textContent.trim();
  273. });
  274. for (let [key, value] of Object.entries(defData)) {
  275. if (nodes[1].textContent.trim() === key.trim()) {
  276. console.log(
  277. `checking ${key}: ${value} ? ${nodes[1].textContent.trim()}`
  278. );
  279. var choices = document.getElementsByClassName("choice");
  280. for (let [index] of Object.keys(choices)) {
  281. var answerChoice = choices[index];
  282. var choiceNodes = answerChoice.textContent.trim().split(", ");
  283. if (choiceNodes.some((k) => value.includes(k))) {
  284. answer(index, "root structure");
  285. return;
  286. }
  287. }
  288. }
  289. }
  290. },
  291. onabort: reportAJAX_Error,
  292. onerror: reportAJAX_Error,
  293. ontimeout: reportAJAX_Error,
  294. });
  295. }
  296. }
  297.  
  298. function checkExampleMultiNode() {
  299. // In case they italize a word we think it's the word that's the answer and then make it look "correct"
  300. var words = document.getElementsByClassName("choice-word");
  301. if (
  302. !Object.values(words).every((word) => {
  303. word.textContent.trim().indexOf(" ") >= 2 ||
  304. word.textContent.trim() == "I'm not sure";
  305. })
  306. ) {
  307. // all of them are words without spaces
  308. //window.console.log("multi-node answers are words");
  309. var bestChoices = {};
  310. for (let iter = 0; iter < words.length; iter++) {
  311. let word = words[iter].textContent.trim();
  312.  
  313. GM_xmlhttpRequest({
  314. method: "GET",
  315. url: `https://membean.com/mywords/${word}`,
  316. responseType: "json",
  317. onload: (rspObj) => {
  318. var parser = new DOMParser();
  319. var htmlDoc = parser.parseFromString(
  320. rspObj.responseText,
  321. "text/html"
  322. );
  323.  
  324. var canswer = htmlDoc.getElementsByClassName("choice answer")[0];
  325. if (canswer) {
  326. var choices = document.getElementsByClassName("choice");
  327. var choicesParsed = {};
  328. for (var i = 0; i < choices.length; i++) {
  329. if (choices[i].textContent === canswer.textContent) {
  330. answer(i, "multi node answer");
  331. return;
  332. } else {
  333. choicesParsed[i] = levenshtein(
  334. choices[i].textContent,
  335. canswer.textContent
  336. ); // parsing the most probable answer
  337. }
  338. }
  339. let bestChoice = Object.keys(choicesParsed).reduce((a, b) =>
  340. choicesParsed[a] > choicesParsed[b] ? a : b
  341. );
  342. if (choicesParsed[bestChoice] >= 20) {
  343. return; // Not a good enough result
  344. }
  345. answer(bestChoice, "best choice parsed for multi node");
  346. }
  347. },
  348. onabort: reportAJAX_Error,
  349. onerror: reportAJAX_Error,
  350. ontimeout: reportAJAX_Error,
  351. });
  352. }
  353. }
  354. }
  355.  
  356. function cdnStoragePlugin() {
  357. var formatter = document.getElementsByClassName(
  358. "single-column-layout with-image"
  359. );
  360. if (formatter.length == 1) {
  361. var img = formatter[0].children[1];
  362. if (img.alt == "constellation question") {
  363. // It is a constellation question - tesseract can't parse so I'm kinda fcked for now
  364. var src = img.src;
  365. GM_xmlhttpRequest({
  366. method: "GET",
  367. url: `${apiURL}/membean/${encodeURIComponent(src)}`,
  368. headers: {
  369. "Content-Type": "application/json",
  370. },
  371. responseType: "json",
  372. onload: (rspObj) => {
  373. (rspObj) => {
  374. if (rspObj.status != 200) {
  375. reportAJAX_Error(rspObj);
  376. return;
  377. }
  378. let data = rspObj.response;
  379. console.log(data);
  380. if (data.answer) {
  381. var choices = document.getElementsByClassName("choice");
  382. for (var iter = 0; iter < choices.length; iter++) {
  383. if (choices[iter].textContent.trim() == data.answer.trim()) {
  384. answer(iter, "stored question");
  385. }
  386. }
  387. window.console.log(
  388. `[${q}] Answer: ${rspObj.response.answer[0].answer}`
  389. );
  390. }
  391. };
  392. },
  393. onabort: reportAJAX_Error,
  394. onerror: reportAJAX_Error,
  395. ontimeout: reportAJAX_Error,
  396. });
  397. }
  398. }
  399. }
  400.  
  401. function definitionOnHoverPlugin() {
  402. // In case they italize a word we think it's the word that's the answer and then make it look "correct"
  403. var words = document.getElementsByClassName("choice-word");
  404. if (
  405. !Object.values(words).every((word) => {
  406. word.textContent.trim().indexOf(" ") >= 2 ||
  407. word.textContent.trim() == "I'm not sure";
  408. })
  409. ) {
  410. // all of them are words without spaces
  411. //window.console.log("answers are sentences");
  412. for (let iter = 0; iter < words.length; iter++) {
  413. let word = words[iter].textContent.trim();
  414. GM_xmlhttpRequest({
  415. method: "GET",
  416. url: `https://membean.com/mywords/${word}`,
  417. responseType: "json",
  418. onload: (rspObj) => {
  419. var parser = new DOMParser();
  420. var htmlDoc = parser.parseFromString(
  421. rspObj.responseText,
  422. "text/html"
  423. );
  424.  
  425. var definition = htmlDoc.getElementsByClassName("def-text");
  426. if (definition.length > 0) {
  427. words[iter].title = definition[0].textContent.trim();
  428.  
  429. // Checking here for filler questions like 1 + _ = 3 and the def we get is 1 + 2 = 3
  430. var processed_question = cache.q.textContent
  431. .replace("_", word)
  432. .replaceAll("_", "")
  433. .trim();
  434. var sentences = Object.values(
  435. htmlDoc
  436. .getElementById("context-paragraph")
  437. .textContent.split(". ")
  438. ).map((val) => val.trim() + ".");
  439. sentences.push(definition[0].textContent.trim());
  440. for (var s_iter = 0; s_iter < sentences.length; s_iter++) {
  441. var similarity = levenshtein(
  442. sentences[s_iter],
  443. processed_question.trim()
  444. );
  445. if (similarity < 40) {
  446. window.console.log(
  447. `Similarity: ${similarity}: ${definition[0].textContent.trim()} / ${processed_question.trim()}`
  448. );
  449. answer(iter, `best similarity for def hover: ${similarity}`);
  450. } else {
  451. //window.console.log(
  452. // `Invalid Hint-Def: ${similarity} - ${sentences[s_iter]} != ${processed_question.trim()}`
  453. // );
  454. }
  455. }
  456. //window.console.log(`Set title for ${word}`);
  457. }
  458. },
  459. onabort: reportAJAX_Error,
  460. onerror: reportAJAX_Error,
  461. ontimeout: reportAJAX_Error,
  462. });
  463. }
  464. } else {
  465. //window.console.log("answers are words");
  466. var hint_nodes = cache.q.children;
  467. if (cache.hint_nodes !== hint_nodes) {
  468. cache.hint_nodes = hint_nodes;
  469. if (hint_nodes.length == 2 && hint_nodes[1].nodeName === "EM") {
  470. var word = hint_nodes[1].textContent;
  471. if (
  472. word[word.length - 1] === "s" &&
  473. !"aeious".split("").includes(word[word.length - 2])
  474. ) {
  475. word = word.slice(0, word.length - 1);
  476. }
  477. window.console.log("Italicised word " + word);
  478. GM_xmlhttpRequest({
  479. method: "GET",
  480. url: `https://membean.com/mywords/${word}`,
  481. responseType: "json",
  482. onload: (rspObj) => {
  483. var parser = new DOMParser();
  484. var htmlDoc = parser.parseFromString(
  485. rspObj.responseText,
  486. "text/html"
  487. );
  488.  
  489. var def = htmlDoc.getElementsByClassName("def-text");
  490.  
  491. if (def.length > 0) {
  492. var processed_question = cache.q.textContent
  493. .replace("_", word)
  494. .replaceAll("_", "");
  495. var sentences = Object.values(
  496. htmlDoc
  497. .getElementById("context-paragraph")
  498. .textContent.split(". ")
  499. ).map((val) => val.trim() + ".");
  500. sentences.push(def[0].textContent.trim());
  501. for (var iter = 0; iter < sentences.length; iter++) {
  502. var similarity = levenshtein(
  503. sentences[iter],
  504. processed_question.trim()
  505. );
  506. if (similarity < 30) {
  507. window.console.log(
  508. `Similarity: ${similarity}: ${definition[0].textContent.trim()} / ${processed_question.trim()}`
  509. );
  510. answer(iter, "best similarity for multi def hover");
  511.  
  512. return;
  513. } else {
  514. //window.console.log(
  515. // `Invalid Hint-Def: ${similarity} - ${
  516. // sentences[iter]
  517. // } != ${processed_question.trim()}`
  518. // );
  519. }
  520. }
  521. cache.q.title = def[0].textContent.trim();
  522. } else {
  523. window.console.log("No def");
  524. }
  525. },
  526. onabort: reportAJAX_Error,
  527. onerror: reportAJAX_Error,
  528. ontimeout: reportAJAX_Error,
  529. });
  530. }
  531. }
  532. }
  533. }
  534.  
  535. function autoTypeNewWordPlugin() {
  536. var letters = document.getElementsByClassName("rest-letters");
  537. if (letters.length >= 1) {
  538. var word = document.getElementById("pronounce-sound");
  539. if (word) {
  540. var text = word.attributes[3].value.split("-")[1];
  541. setTimeout(() => (document.getElementById("choice").value = text), 1000);
  542. }
  543. }
  544. }
  545.  
  546. function autoAnswerTextInputPlugin() {
  547. var valid = document.getElementsByClassName("single-column-layout cloze");
  548.  
  549. if (valid.length > 0) {
  550. var v = valid[0];
  551. if (v === cache.v) {
  552. return;
  553. }
  554. cache.v = v;
  555. console.log("typing question");
  556. //setTimeout(() => document.getElementById("notsure").click(), 600);
  557. }
  558. }
  559.  
  560. function autoStartNewSessionPlugin() {
  561. var valid = document.getElementsByClassName("button-to");
  562. if (valid.length > 0) {
  563. Object.values(valid).reverse()[0].children[0].click();
  564. }
  565. }
  566.  
  567. function processJSON_Response(rspObj) {
  568. if (rspObj.status != 200) {
  569. reportAJAX_Error(rspObj);
  570. } else {
  571. window.console.log(rspObj.responseText);
  572. }
  573. }
  574.  
  575. function reportAJAX_Error(rspObj) {
  576. console.log(rspObj);
  577. window.console.log(
  578. `TM scrpt (membean-tracker) => Error ${rspObj.status}! ${rspObj.statusText} (${rspObj.url})`
  579. );
  580. }
  581.  
  582. const levenshtein = function (a, b) {
  583. // difference between strings for helping w/ similar membean answers
  584. if (a.length == 0) return b.length;
  585. if (b.length == 0) return a.length;
  586.  
  587. // swap to save some memory O(min(a,b)) instead of O(a)
  588. if (a.length > b.length) {
  589. var tmp = a;
  590. a = b;
  591. b = tmp;
  592. }
  593.  
  594. var row = [];
  595. // init the row
  596. for (var i = 0; i <= a.length; i++) {
  597. row[i] = i;
  598. }
  599.  
  600. // fill in the rest
  601. for (var z = 1; z <= b.length; z++) {
  602. var prev = z;
  603. for (var j = 1; j <= a.length; j++) {
  604. var val;
  605. if (b.charAt(z - 1) == a.charAt(j - 1)) {
  606. val = row[j - 1]; // match
  607. } else {
  608. val = Math.min(
  609. row[j - 1] + 1, // substitution
  610. prev + 1, // insertion
  611. row[j] + 1
  612. ); // deletion
  613. }
  614. row[j - 1] = prev;
  615. prev = val;
  616. }
  617. row[a.length] = prev;
  618. }
  619.  
  620. return row[a.length];
  621. };