Membean Tracker

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

// ==UserScript==
// @name         Membean Tracker
// @namespace    http://tampermonkey.net/
// @version      0.1.0
// @description  A powerful membean helper that uses a variety of tools to answer membean questions.
// @author       Squidtoon99 (https://squid.pink)
// @include        https://membean.com/training_sessions/*/user_state
// @include        https://membean.com/mywords/*
// @include        https://membean.com/training_sessions/new
// @icon         https://www.google.com/s2/favicons?domain=membean.com
// @grant        GM_xmlhttpRequest
// @connect      squid.pink
// ==/UserScript==
var cache = {};
var apiURL = "https://api.squid.pink";
var q;

const plugins = [
  autoSelectHTMLPlugin,
  questionPlugin,
  checkExampleSingleNode,
  checkExampleMultiNode,
  definitionOnHoverPlugin,
  autoNextPlugin,
];
const persistent_plugins = [storeCorrectAnswerPlugin, autoTypeNewWordPlugin];

const non_question_plugins = [
  autoAnswerTextInputPlugin,
  autoStartNewSessionPlugin,
];

setInterval(() => {
  // Checking if there is a new question
  var questions = document.getElementsByClassName("question");
  if (questions.length > 0) {
    var question = questions[0];
    var iter;
    if (question !== q) {
      q = question;
      cache.q = question;
      iter = plugins; // the plugins that only need to run when the question renders
      cache.answer = false;
    } else {
      iter = persistent_plugins; // the plugins that need to be constantly running
    }
    for (var z = 0; z < iter.length; z++) {
      iter[z]();
    }
    if (cache.answer == false) {
      var choices = document.getElementsByClassName("choice");
      setTimeout(() => {
        if (
          choices === document.getElementsByClassName("choice") &&
          document.getElementsByClassName("choice correct").length == 0
        ) {
          //console.log("autoclicking");
          //document.getElementsByClassName("choice")[0].click();
        }
      }, 500);
    }
  } else {
    for (var x = 0; x < non_question_plugins.length; x++) {
      non_question_plugins[x]();
    }
  }
  if (Math.floor(Math.random() * 1000) === 5) {
    location.reload();
    window.console.log("reloading");
  }
}, 500);

setInterval(() => {
  if (document.getElementsByClassName("take_a_break").length == 1) {
    console.log("taking a break");
    document.getElementById("Click_me_to_stop").click();
    setTimeout(
      () =>
        (window.location.href = "https://membean.com/training_sessions/new"),
      1000
    );
  }
}, 1000);

function answer(answer_num, reason) {
  var o = Object.values(document.getElementsByClassName("choice"))[answer_num];
  cache.answer = true;
  console.log(`Found correct answer (${answer_num}): [${reason}]`);
  o.className = "choice correct";
  setTimeout(() => o.click(), 8000); // set this timeout to how long you want to wait (ms) before answering questions
  //o.click();
}

function autoNextPlugin() {
  var c = document.getElementById("next-btn");
  if (c) {
    setTimeout(() => c.click(), 1000);
  }
}

function autoSelectHTMLPlugin() {
  // This plugin simply will select the choice answer html selector
  var c = document.getElementsByClassName("choice answer");
  if (c.length > 0) {
    c[0].click();
  }
}

function questionPlugin() {
  // This queries the api to see if there is a value stored
  var url = `${apiURL}/membean/${encodeURIComponent(q.textContent.trim())}`;
  GM_xmlhttpRequest({
    method: "GET",
    url: url,
    headers: {
      "Content-Type": "application/json",
    },
    responseType: "json",
    onload: (rspObj) => {
      if (rspObj.status == 404) {
        // temp autoclicking
        var c = document.getElementsByClassName("choice");
        
        return;
      } else if (rspObj.status != 200) {
        reportAJAX_Error(rspObj);
        return;
      }
      let data = rspObj.response;
      console.log(data);
      if (data.answer) {
        var choices = document.getElementsByClassName("choice");
        for (var iter = 0; iter < choices.length; iter++) {
          if (choices[iter].textContent.trim() == data.answer.trim()) {
            answer(iter, "stored question");
          }
        }
        window.console.log(`[${q}] Answer: ${data.answer[0].answer}`);
      } else {
        window.console.log("No storage for this question");
      }
    },
    onabort: reportAJAX_Error,
    onerror: reportAJAX_Error,
    ontimeout: reportAJAX_Error,
  });
}

function storeCorrectAnswerPlugin() {
  // stores the correct answer whenever html is updated
  var formatter = document.getElementsByClassName(
    "single-column-layout with-image"
  );
  var correct = document.getElementsByClassName("choice correct");
  if (correct.length > 0) {
    var e = correct[0];
    if (cache.e === e) {
      return;
    }
    cache.e = e;
    var data;
    if (formatter.length == 1) {
      // Images suck why membean why
      var img = formatter[0].children[1];
      data = { question: img.src, answer: e.textContent };
    } else {
      // Not an image question just a standard one
      data = {
        question: cache.q.textContent.trim(),
        answer: e.textContent.trim(),
      };
    }
    if (data.question.includes("Try again!")) {
      return;
    }
    GM_xmlhttpRequest({
      method: "POST",
      url: `${apiURL}/membean/`,
      data: JSON.stringify(data),
      headers: {
        "Content-Type": "application/json",
      },
      responseType: "json",
      onload: (rspObj) => {
        if (rspObj.status == 201) {
          window.console.log("Stored answer for: " + data.question);
        } else {
          console.log(rspObj);
        }
      },
      onabort: reportAJAX_Error,
      onerror: reportAJAX_Error,
      ontimeout: reportAJAX_Error,
    });
  }
}

function checkExampleSingleNode() {
  // In case they italize a word we think it's the word that's the answer and then make it look "correct"
  var nodes = cache.q.children;
  if (nodes.length == 2 && nodes[1].nodeName === "EM") {
    var word = nodes[1].textContent;
    if (
      word[word.length - 1] === "s" &&
      !"aeious".split("").includes(word[word.length - 2])
    ) {
      word = word.slice(0, word.length - 1);
    }
    if (word.replaceAll("_", "").trim() === "") {
      return;
    }
    console.log(`Question about ${word}`);
    GM_xmlhttpRequest({
      method: "GET",
      url: `https://membean.com/mywords/${word}`,
      responseType: "json",
      onload: (rspObj) => {
        var parser = new DOMParser();
        var htmlDoc = parser.parseFromString(rspObj.responseText, "text/html");

        var canswer = htmlDoc.getElementsByClassName("choice answer")[0];

        if (!canswer) {
          return;
        }

        var choices = document.getElementsByClassName("choice");
        var choicesParsed = {};
        for (var i = 0; i < choices.length; i++) {
          if (choices[i].textContent === canswer.textContent) {
            answer(i, "singleNode example");
            return;
          } else {
            choicesParsed[i] = levenshtein(
              choices[i].textContent,
              canswer.textContent
            ); // parsing the most probable answer
          }
        }
        let bestChoice = Object.keys(choicesParsed).reduce((a, b) =>
          choicesParsed[a] > choicesParsed[b] ? a : b
        );
        if (
          choicesParsed[bestChoice] >= 10 ||
          bestChoice == Object.keys(bestChoice).length - 1
        ) {
          return; // Not a good enough result
        }
        answer(bestChoice, "single node best example");
      },
      onabort: reportAJAX_Error,
      onerror: reportAJAX_Error,
      ontimeout: reportAJAX_Error,
    });
  } else if (nodes.length === 3 && nodes[1].nodeName === "EM") {
    word =
      document.getElementsByClassName("question")[0].children[2].textContent;
    console.log("stupid question about roots: " + word);
    GM_xmlhttpRequest({
      method: "GET",
      url: `https://membean.com/mywords/${word}`,
      responseType: "json",
      onload: (rspObj) => {
        var parser = new DOMParser();
        var htmlDoc = parser.parseFromString(rspObj.responseText, "text/html");

        var defData = {};
        Object.values(
          htmlDoc.getElementById("word-structure").children[1].children[0]
            .children[0].children
        ).forEach((key) => {
          defData[key.children[0].textContent.trim()] =
            key.children[2].textContent.trim();
        });
        for (let [key, value] of Object.entries(defData)) {
          if (nodes[1].textContent.trim() === key.trim()) {
            console.log(
              `checking ${key}: ${value} ? ${nodes[1].textContent.trim()}`
            );
            var choices = document.getElementsByClassName("choice");
            for (let [index] of Object.keys(choices)) {
              var answerChoice = choices[index];
              var choiceNodes = answerChoice.textContent.trim().split(", ");
              if (choiceNodes.some((k) => value.includes(k))) {
                answer(index, "root structure");
                return;
              }
            }
          }
        }
      },
      onabort: reportAJAX_Error,
      onerror: reportAJAX_Error,
      ontimeout: reportAJAX_Error,
    });
  }
}

function checkExampleMultiNode() {
  // In case they italize a word we think it's the word that's the answer and then make it look "correct"
  var words = document.getElementsByClassName("choice-word");
  if (
    !Object.values(words).every((word) => {
      word.textContent.trim().indexOf(" ") >= 2 ||
        word.textContent.trim() == "I'm not sure";
    })
  ) {
    // all of them are words without spaces
    //window.console.log("multi-node answers are words");
    var bestChoices = {};
    for (let iter = 0; iter < words.length; iter++) {
      let word = words[iter].textContent.trim();

      GM_xmlhttpRequest({
        method: "GET",
        url: `https://membean.com/mywords/${word}`,
        responseType: "json",
        onload: (rspObj) => {
          var parser = new DOMParser();
          var htmlDoc = parser.parseFromString(
            rspObj.responseText,
            "text/html"
          );

          var canswer = htmlDoc.getElementsByClassName("choice answer")[0];
          if (canswer) {
            var choices = document.getElementsByClassName("choice");
            var choicesParsed = {};
            for (var i = 0; i < choices.length; i++) {
              if (choices[i].textContent === canswer.textContent) {
                answer(i, "multi node answer");
                return;
              } else {
                choicesParsed[i] = levenshtein(
                  choices[i].textContent,
                  canswer.textContent
                ); // parsing the most probable answer
              }
            }
            let bestChoice = Object.keys(choicesParsed).reduce((a, b) =>
              choicesParsed[a] > choicesParsed[b] ? a : b
            );
            if (choicesParsed[bestChoice] >= 20) {
              return; // Not a good enough result
            }
            answer(bestChoice, "best choice parsed for multi node");
          }
        },
        onabort: reportAJAX_Error,
        onerror: reportAJAX_Error,
        ontimeout: reportAJAX_Error,
      });
    }
  }
}

function cdnStoragePlugin() {
  var formatter = document.getElementsByClassName(
    "single-column-layout with-image"
  );
  if (formatter.length == 1) {
    var img = formatter[0].children[1];
    if (img.alt == "constellation question") {
      // It is a constellation question - tesseract can't parse so I'm kinda fcked for now
      var src = img.src;
      GM_xmlhttpRequest({
        method: "GET",
        url: `${apiURL}/membean/${encodeURIComponent(src)}`,
        headers: {
          "Content-Type": "application/json",
        },
        responseType: "json",
        onload: (rspObj) => {
          (rspObj) => {
            if (rspObj.status != 200) {
              reportAJAX_Error(rspObj);
              return;
            }
            let data = rspObj.response;
            console.log(data);
            if (data.answer) {
              var choices = document.getElementsByClassName("choice");
              for (var iter = 0; iter < choices.length; iter++) {
                if (choices[iter].textContent.trim() == data.answer.trim()) {
                  answer(iter, "stored question");
                }
              }
              window.console.log(
                `[${q}] Answer: ${rspObj.response.answer[0].answer}`
              );
            }
          };
        },
        onabort: reportAJAX_Error,
        onerror: reportAJAX_Error,
        ontimeout: reportAJAX_Error,
      });
    }
  }
}

function definitionOnHoverPlugin() {
  // In case they italize a word we think it's the word that's the answer and then make it look "correct"
  var words = document.getElementsByClassName("choice-word");
  if (
    !Object.values(words).every((word) => {
      word.textContent.trim().indexOf(" ") >= 2 ||
        word.textContent.trim() == "I'm not sure";
    })
  ) {
    // all of them are words without spaces
    //window.console.log("answers are sentences");
    for (let iter = 0; iter < words.length; iter++) {
      let word = words[iter].textContent.trim();
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://membean.com/mywords/${word}`,
        responseType: "json",
        onload: (rspObj) => {
          var parser = new DOMParser();
          var htmlDoc = parser.parseFromString(
            rspObj.responseText,
            "text/html"
          );

          var definition = htmlDoc.getElementsByClassName("def-text");
          if (definition.length > 0) {
            words[iter].title = definition[0].textContent.trim();

            // Checking here for filler questions like 1 + _ = 3 and the def we get is 1 + 2 = 3
            var processed_question = cache.q.textContent
              .replace("_", word)
              .replaceAll("_", "")
              .trim();
            var sentences = Object.values(
              htmlDoc
                .getElementById("context-paragraph")
                .textContent.split(". ")
            ).map((val) => val.trim() + ".");
            sentences.push(definition[0].textContent.trim());
            for (var s_iter = 0; s_iter < sentences.length; s_iter++) {
              var similarity = levenshtein(
                sentences[s_iter],
                processed_question.trim()
              );
              if (similarity < 40) {
                window.console.log(
                  `Similarity: ${similarity}: ${definition[0].textContent.trim()} / ${processed_question.trim()}`
                );
                answer(iter, `best similarity for def hover: ${similarity}`);
              } else {
                //window.console.log(
                //     `Invalid Hint-Def: ${similarity} - ${sentences[s_iter]} != ${processed_question.trim()}`
                // );
              }
            }
            //window.console.log(`Set title for ${word}`);
          }
        },
        onabort: reportAJAX_Error,
        onerror: reportAJAX_Error,
        ontimeout: reportAJAX_Error,
      });
    }
  } else {
    //window.console.log("answers are words");
    var hint_nodes = cache.q.children;
    if (cache.hint_nodes !== hint_nodes) {
      cache.hint_nodes = hint_nodes;
      if (hint_nodes.length == 2 && hint_nodes[1].nodeName === "EM") {
        var word = hint_nodes[1].textContent;
        if (
          word[word.length - 1] === "s" &&
          !"aeious".split("").includes(word[word.length - 2])
        ) {
          word = word.slice(0, word.length - 1);
        }
        window.console.log("Italicised word " + word);
        GM_xmlhttpRequest({
          method: "GET",
          url: `https://membean.com/mywords/${word}`,
          responseType: "json",
          onload: (rspObj) => {
            var parser = new DOMParser();
            var htmlDoc = parser.parseFromString(
              rspObj.responseText,
              "text/html"
            );

            var def = htmlDoc.getElementsByClassName("def-text");

            if (def.length > 0) {
              var processed_question = cache.q.textContent
                .replace("_", word)
                .replaceAll("_", "");
              var sentences = Object.values(
                htmlDoc
                  .getElementById("context-paragraph")
                  .textContent.split(". ")
              ).map((val) => val.trim() + ".");
              sentences.push(def[0].textContent.trim());
              for (var iter = 0; iter < sentences.length; iter++) {
                var similarity = levenshtein(
                  sentences[iter],
                  processed_question.trim()
                );
                if (similarity < 30) {
                  window.console.log(
                    `Similarity: ${similarity}: ${definition[0].textContent.trim()} / ${processed_question.trim()}`
                  );
                  answer(iter, "best similarity for multi def hover");

                  return;
                } else {
                  //window.console.log(
                  //              `Invalid Hint-Def: ${similarity} - ${
                  //  sentences[iter]
                  //                      } != ${processed_question.trim()}`
                  //   );
                }
              }
              cache.q.title = def[0].textContent.trim();
            } else {
              window.console.log("No def");
            }
          },
          onabort: reportAJAX_Error,
          onerror: reportAJAX_Error,
          ontimeout: reportAJAX_Error,
        });
      }
    }
  }
}

function autoTypeNewWordPlugin() {
  var letters = document.getElementsByClassName("rest-letters");
  if (letters.length >= 1) {
    var word = document.getElementById("pronounce-sound");
    if (word) {
      var text = word.attributes[3].value.split("-")[1];
      setTimeout(() => (document.getElementById("choice").value = text), 1000);
    }
  }
}

function autoAnswerTextInputPlugin() {
  var valid = document.getElementsByClassName("single-column-layout cloze");

  if (valid.length > 0) {
    var v = valid[0];
    if (v === cache.v) {
      return;
    }
    cache.v = v;
    console.log("typing question");
    //setTimeout(() => document.getElementById("notsure").click(), 600);
  }
}

function autoStartNewSessionPlugin() {
  var valid = document.getElementsByClassName("button-to");
  if (valid.length > 0) {
    Object.values(valid).reverse()[0].children[0].click();
  }
}

function processJSON_Response(rspObj) {
  if (rspObj.status != 200) {
    reportAJAX_Error(rspObj);
  } else {
    window.console.log(rspObj.responseText);
  }
}

function reportAJAX_Error(rspObj) {
  console.log(rspObj);
  window.console.log(
    `TM scrpt (membean-tracker) => Error ${rspObj.status}!  ${rspObj.statusText} (${rspObj.url})`
  );
}

const levenshtein = function (a, b) {
  // difference between strings for helping w/ similar membean answers
  if (a.length == 0) return b.length;
  if (b.length == 0) return a.length;

  // swap to save some memory O(min(a,b)) instead of O(a)
  if (a.length > b.length) {
    var tmp = a;
    a = b;
    b = tmp;
  }

  var row = [];
  // init the row
  for (var i = 0; i <= a.length; i++) {
    row[i] = i;
  }

  // fill in the rest
  for (var z = 1; z <= b.length; z++) {
    var prev = z;
    for (var j = 1; j <= a.length; j++) {
      var val;
      if (b.charAt(z - 1) == a.charAt(j - 1)) {
        val = row[j - 1]; // match
      } else {
        val = Math.min(
          row[j - 1] + 1, // substitution
          prev + 1, // insertion
          row[j] + 1
        ); // deletion
      }
      row[j - 1] = prev;
      prev = val;
    }
    row[a.length] = prev;
  }

  return row[a.length];
};