Membean Tracker

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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];
};