Blooket Bot Blocker (BBB)

Block Bots in Blooket!

// ==UserScript==
// @name         Blooket Bot Blocker (BBB)
// @namespace    https://tameprmonkey.net/
// @version      12.0
// @description  Block Bots in Blooket!
// @author       thundercatcher
// @match        *://*dashboard.blooket.com/play/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// ==/UserScript==

if (
  window.fireLoaded ||
  window.parent?.kantibotEnabled ||
  window.parent?.fireLoaded
) {
  throw "[ANTIBOT] - page is loaded";
}
if (window.localStorage.extraCheck) {
  console.log("[ANTIBOT] - Detected PIN Checker");
}
if (window.localStorage.kahootThemeScript) {
  console.log("[ANTIBOT] - Detected KonoSuba Theme");
}

let writePromise = new Promise(() => {});

// Should allow for default behavior and reload page
if (location.pathname.includes("/oauth2/")) {
  setTimeout(() => {
    location.reload();
  }, 3e3);
} else {
  writePromise = new Promise((res) =>
    setTimeout(() => {
      document.write(`
      <p id="antibot-loading-notice">[ANTIBOT] - Patching Blooket. Please wait.</p>
      <p>If screen stays blank for a long time, report an issue in <a href="https://discord.gg/TSBEk5yzU8">Discord</a>.</p>
     `);
      res();
    }, 250)
  );
}
window.antibotAdditionalScripts = window.antibotAdditionalScripts || [];
window.antibotAdditionalReplacements =
  window.antibotAdditionalReplacements || [];
window.kantibotEnabled = true;

/**
 * External Libraries
 * Note: This script requires https://raw.githubusercontent.com/theusaf/a-set-of-english-words/master/index.js
 * - This is a script that loads 275k english words into a set. (about 30MB ram?)
 * @see https://github.com/theusaf/a-set-of-english-words
 *
 * Also, it requires https://raw.githubusercontent.com/theusaf/random-name/master/names.js
 * - Loads a bunch of names into sets.
 * @see https://raw.githubusercontent.com/theusaf/random-name
 *
 * If these get removed or fail to load, this should not break the service, but will cause certain features to not work
 */
const url = window.location.href,
  requiredAssets = [
    "https://raw.githubusercontent.com/theusaf/a-set-of-english-words/master/index.js",
    "https://raw.githubusercontent.com/theusaf/random-name/master/names.js"
  ],
  importBlobURLs = {};

window.importBlobURLs = importBlobURLs;

/**
 * Creates a blob url from a string.
 *
 * @param {string} script The text to convert to a blob url
 * @returns {string} The blob url
 */
function createBlobURL(script) {
  return URL.createObjectURL(
    new Blob([script], { type: "application/javascript" })
  );
}

/**
 * Patches the imported url and resolves with the patched script.
 *
 * @param {string} url The url being imported
 * @returns {Promise<any>} The imported script's exports
 */
async function antibotImport(url) {
  const importBlobURLs =
      window.importBlobURLs ??
      window.parent?.importBlobURLs ??
      window.windw?.importBlobURLs,
    makeHttpRequest =
      window.kantibotMakeHTTPRequest ??
      window.parent?.kantibotMakeHTTPRequest ??
      window.windw?.kantibotMakeHTTPRequest,
    createBlobURL =
      window.kantibotCreateBlobURL ??
      window.parent?.kantibotCreateBlobURL ??
      window.windw?.kantibotCreateBlobURL;

  console.log(`[ANTIBOT] - Handling import of ${url}`);
  // We need to intercept any requests and modify them!
  if (url.startsWith(".")) {
    url = `https://assets-cdn.kahoot.it/player/v2/assets${url.substring(1)}`;
  }
  if (importBlobURLs[url]) {
    console.log(`[ANTIBOT] - ${url} already exists.`);
    return import(importBlobURLs[url]);
  } else {
    // check if this url needs to be patched!
    let { response: moduleCode } = await makeHttpRequest(url),
      needsEdit = false;
    const importFunctionRegex = /\bimport\b\(/g,
      importStatementRegex =
        /\bimport\b[\w.\-{}\s[\],:]*?\bfrom\b"[\w.\-/]*?"/g;

    // if it has dynamic import statements
    if (importFunctionRegex.test(moduleCode)) {
      needsEdit = true;
      moduleCode = moduleCode.replace(importFunctionRegex, "antibotImport(");
      moduleCode = `${antibotImport.toString()}${moduleCode}`;
    }
    // if it has a regular import statement
    if (importStatementRegex.test(moduleCode)) {
      // Check if url exists for the import
      for (const imp of moduleCode.match(importStatementRegex)) {
        const [, impURL] = imp.match(/"([\w.\-/]*?)"/);
        let editedImportURL = impURL;
        if (editedImportURL.startsWith(".")) {
          editedImportURL = `https://assets-cdn.kahoot.it/player/v2/assets${editedImportURL.substring(
            1
          )}`;
        }
        if (importBlobURLs[editedImportURL]) {
          needsEdit = true;
          moduleCode = moduleCode.replace(
            impURL,
            importBlobURLs[editedImportURL]
          );
        }
      }
    }
    if (needsEdit) {
      // Create a blob url and import it!
      console.log(`[ANTIBOT] - Modifying ${url} and creating blob url.`);
      const blobURL = createBlobURL(moduleCode);
      importBlobURLs[url] = blobURL;
      return import(blobURL);
    } else {
      console.log(`[ANTIBOT] - ${url} does not require a blob version.`);
      // So we don't keep checking it each time
      importBlobURLs[url] = url;
      return import(url);
    }
  }
}

/**
 * Makes an http request, and resolves with the request after the response is received.
 *
 * @param {string} url The url to request
 * @returns {Promise<XMLHttpRequest>}
 */
function makeHttpRequest(url) {
  const request = new XMLHttpRequest();
  request.open("GET", url);
  request.send();
  return new Promise((resolve, reject) => {
    request.onerror = request.onload = () => {
      if (request.readyState === 4 && request.status === 200) {
        resolve(request);
      } else {
        reject(request);
      }
    };
  });
}

/**
 * Fetches the main page, and fetches the assets to be modified.
 *
 * @returns {Promise<{
 *   page: string,
 *   mainScriptURL: string
 * }>} The page's content (patched) and the main script's url
 */
async function fetchMainPage() {
  const mainPageRequest = await makeHttpRequest(url),
    [mainScriptURL] = mainPageRequest.response.match(
      /\/\/assets-cdn.*\/v2\/assets\/index.*?(?=")/
    ),
    originalPage = mainPageRequest.response.replace(
      /<script type="module".*?<\/script>/,
      ""
    );
  return {
    page: originalPage,
    mainScriptURL: mainScriptURL.startsWith("//")
      ? `https:${mainScriptURL}`
      : mainScriptURL
  };
}

/**
 * Fetches the main script and patches it to gain access to internal data.
 *
 * @param {string} mainScriptURL The url of the main script
 * @returns {Promise<string>} The main script (patched)
 */
async function fetchMainScript(mainScriptURL) {
  const mainScriptRequest = await makeHttpRequest(mainScriptURL);
  let mainScript = mainScriptRequest.response;
  // Access the currentQuestionTimer and change the question time
  const currentQuestionTimerRegex =
      /currentQuestionTimer:([$\w]+)\.payload\.questionTime/,
    [, currentQuestionTimerLetter] = mainScript.match(
      currentQuestionTimerRegex
    );
  mainScript = mainScript.replace(
    currentQuestionTimerRegex,
    `currentQuestionTimer:${currentQuestionTimerLetter}.payload.questionTime + (()=>{
      return (windw.antibotData.settings.teamtimeout * 1000) || 0;
    })()`
  );

  // Access global functions. Also gains direct access to the controllers?
  const globalFuncRegex =
      /\({[^"`]*?quiz[^"`]*?startQuiz:([$\w]+).*?}\)=>{(?=var)/,
    [globalFuncMatch, globalFuncLetter] = mainScript.match(globalFuncRegex);
  mainScript = mainScript.replace(
    globalFuncRegex,
    `${globalFuncMatch}windw.antibotData.kahootInternals.globalFuncs = {startQuiz:${globalFuncLetter}};`
  );
  // Access the fetched quiz information. Allows the quiz to be modified when the quiz is fetched!
  // Note to future maintainer: if code switches back to using a function(){} rather than arrow function, see v3.1.5
  const fetchedQuizInformationRegex =
      /RETRIEVE_KAHOOT_ERROR",.*?{response:([$\w]+)}\)/,
    [fetchedQuizInformationCode, fetchedQuizInformationLetter] =
      mainScript.match(fetchedQuizInformationRegex);
  mainScript = mainScript.replace(
    fetchedQuizInformationRegex,
    `RETRIEVE_KAHOOT_ERROR",${
      fetchedQuizInformationCode
        .split('RETRIEVE_KAHOOT_ERROR",')[1]
        .split("response:")[0]
    }response:(()=>{
      windw.antibotData.kahootInternals.globalQuizData = ${fetchedQuizInformationLetter};
      windw.antibotData.methods.extraQuestionSetup(${fetchedQuizInformationLetter});
      return ${fetchedQuizInformationLetter};
    })()})`
  );
  // Access the core data
  const coreDataRegex = /([$\w]+)\.game\.core/,
    [, coreDataLetter] = mainScript.match(coreDataRegex);
  mainScript = mainScript.replace(
    coreDataRegex,
    `(()=>{
      if(typeof windw !== "undefined"){
        windw.antibotData.kahootInternals.kahootCore = ${coreDataLetter};
      }
      return ${coreDataLetter}.game.core;
    })()`
  );
  // Access game settings (somehow removed from the core data...)
  // 3.2.8 --> added back to core data
  // This code doesn't actually do anything, but is kept in case
  const gameSettingsRegex = /getGameOptions\(\){/;
  mainScript = mainScript.replace(
    gameSettingsRegex,
    `getGameOptions() {
      if (typeof windw !== "undefined") {
        windw.antibotData.kahootInternals.gameOptions = this;
      }`
  );
  // Access two factor stuff
  const twoFactorRegex = /(const [$\w]+=)7,/;
  mainScript = mainScript.replace(
    twoFactorRegex,
    `${mainScript.match(twoFactorRegex)[1]}(()=>{
      const antibotConfig = JSON.parse(localStorage.antibotConfig || "{}"),
        {twoFactorTime} = antibotConfig;
      if (twoFactorTime) {
        return +twoFactorTime;
      } else {
        return 7;
      }
    })(),`
  );
  // Overwrite navigation (opens in parent, preventing iframe issues)
  const navigationRegex =
      /prototype\.navigate=function\(([$\w]+)\){(.{1,80}?window\.location\.replace)/,
    [, navigationLetter, navigationOriginalCode] =
      mainScript.match(navigationRegex);
  mainScript = mainScript.replace(
    navigationRegex,
    `prototype.navigate = function(${navigationLetter}) {
      if (${navigationLetter}.url && typeof windw !== "undefined") {
        windw.location.replace(${navigationLetter}.url);
        return;
      }
      ${navigationOriginalCode}`
  );

  // access message socket
  // moved from "vendors"
  const patchedScriptRegex = /\.onMessage=function\(([$\w]+),([$\w]+)\)\{/,
    [, websocketMessageLetter1, websocketMessageLetter2] =
      mainScript.match(patchedScriptRegex);
  mainScript = mainScript.replace(
    patchedScriptRegex,
    `.onMessage = function(${websocketMessageLetter1},${websocketMessageLetter2}){
      windw.antibotData.methods.websocketMessageHandler(${websocketMessageLetter1},${websocketMessageLetter2});`
  );

  // other replacements
  for (const func of window.antibotAdditionalReplacements) {
    mainScript = func(mainScript);
  }

  // modified import
  mainScript = mainScript.replace(/\bimport\b\(/g, "antibotImport(");

  mainScript = `${antibotImport.toString()}${mainScript}`;

  return mainScript;
}

const kantibotProgramCode = () => {
  class EvilBotJoinedError extends Error {
    constructor() {
      super("Bot Banned, Ignore Join");
    }
  }

  class AnsweredTooQuicklyError extends Error {
    constructor() {
      super("Answer was too quick!");
    }
  }

  const windw = window.parent;
  window.windw = windw;

  /**
   * createSetting - Creates a setting option string
   *
   * @param  {String}   name         The name of the option
   * @param  {String}   type         The type of the option
   * @param  {String}   id           The id of the option
   * @param  {String}   description  The description of the option
   * @param  {String}   default      The default value of the option
   * @param  {Function} setup        A function to modify the input,label
   * @param  {Function} callback     A function to call when the value changes
   * @returns {String}               The resulting HTML for the option
   */
  function createSetting(
    name,
    type,
    id,
    description,
    def = null,
    setup = () => {},
    callback = () => {}
  ) {
    const label = document.createElement("label"),
      input =
        type === "textarea"
          ? document.createElement("textarea")
          : document.createElement("input"),
      container = document.createElement("div");
    if (type !== "textarea") {
      input.setAttribute("type", type);
    } else {
      input.setAttribute(
        "onclick",
        `
        this.className = "antibot-textarea";
        `
      );
      input.setAttribute(
        "onblur",
        `
        this.className = "";
        `
      );
    }
    input.id = label.htmlFor = `antibot.config.${id}`;
    label.id = input.id + ".label";
    label.title = description;
    label.innerHTML = name;
    if (type === "checkbox") {
      container.append(input, label);
      input.setAttribute(
        "onclick",
        `
        const value = event.target.checked;
        windw.antibotData.methods.setSetting(event.target.id.match(/\\w+$/)[0], value);
        (${callback.toString()})(event.target);
        `
      );
    } else {
      container.append(label, input);
      input.setAttribute(
        "onchange",
        `
        const value = event.target.nodeName === "TEXTAREA" ? event.target.value.split("\\n") : event.target.type === "number" ? +event.target.value : event.target.value;
        windw.antibotData.methods.setSetting(event.target.id.match(/\\w+$/)[0], value);
        (${callback.toString()})(event.target);
        `
      );
      label.className = "antibot-input";
    }
    if (def != null) {
      if (type === "checkbox") {
        if (def) {
          input.setAttribute("checked", "");
        }
      } else {
        input.setAttribute("value", `${def}`);
      }
    }
    setup(input, label);
    return container.outerHTML;
  }

  // create watermark
  const UITemplate = document.createElement("template");
  UITemplate.innerHTML = `<div id="antibotwtr">
    <p>v3.5.0 ©theusaf</p>
    <p id="antibot-killcount">0</p>
    <details>
      <summary>config</summary>
      <div id="antibot-settings">
${createSetting(
  "Block Fast Answers",
  "checkbox",
  "timeout",
  "Blocks answers sent 0.5 seconds after the question starts"
)}
${createSetting(
  "Block Random Names",
  "checkbox",
  "looksRandom",
  "Blocks names that look random, such as 'rAnDOM naMe'",
  true
)}
${createSetting(
  "Block Format F[.,-]L",
  "checkbox",
  "blockformat1",
  "Blocks names using the format [First][random char][Last]",
  true
)}
${createSetting(
  "Additional Blocking Filters",
  "checkbox",
  "blockservice1",
  "Enables multiple additional blocking filters for some bot programs"
)}
${createSetting(
  "Block Numbers",
  "checkbox",
  "blocknum",
  "Blocks names containing numbers, if multiple with numbers join within a short period of time"
)}
${createSetting(
  "Force Alphanumeric Names",
  "checkbox",
  "forceascii",
  "Blocks names containing non-alphanumeric characters, if multiple join within a short period of time"
)}
${createSetting(
  "Detect Patterns",
  "checkbox",
  "patterns",
  "Blocks bots spammed using similar patterns"
)}
${createSetting(
  "Additional Question Time",
  "number",
  "teamtimeout",
  "Adds extra seconds to a question",
  0,
  (input) => input.setAttribute("step", 1)
)}
${createSetting(
  "Two-Factor Auth Timer",
  "number",
  "twoFactorTime",
  "Specify the number of seconds for the two-factor auth. The first iteration will use the default 7 seconds, then will use your input",
  7,
  (input) => {
    input.setAttribute("step", 1);
    input.setAttribute("min", 1);
  },
  () => {
    windw.antibotData.methods.kahootAlert(
      "Changes will only take effect upon reload."
    );
  }
)}
${createSetting(
  "Name Match Percent",
  "number",
  "percent",
  "The percent to check name similarity before banning the bot.",
  0.6,
  (input) => input.setAttribute("step", 0.1)
)}
${createSetting(
  "Word Blacklist",
  "textarea",
  "wordblock",
  "Block names containing any from a list of words. Separate by new line."
)}
${createSetting(
  "Auto-Lock Threshold",
  "number",
  "ddos",
  "Specify the number of bots/minute to lock the game. Set to 0 to disable",
  0,
  (input) => input.setAttribute("step", 1)
)}
${createSetting(
  "Lobby Auto-Start Time",
  "number",
  "start_lock",
  "Specify the maximum amount of time for a lobby to stay open after a player joins. Set to 0 to disable",
  0,
  (input) => input.setAttribute("step", 1)
)}
${createSetting(
  "Show Antibot Timers",
  "checkbox",
  "counters",
  "Display Antibot Counters/Timers (Lobby Auto-Start, Auto-Lock, etc)"
)}
${createSetting(
  "Counter Kahoot! Cheats",
  "checkbox",
  "counterCheats",
  "Adds an additional 5 second question at the end to counter cheats. Changing this mid-game may break the game or will not apply until refresh",
  null,
  undefined,
  () => {
    windw.antibotData.methods.kahootAlert(
      "Changes may only take effect upon reload."
    );
  }
)}
${createSetting(
  "Enable CAPTCHA",
  "checkbox",
  "enableCAPTCHA",
  "Adds a 30 second poll at the start of the quiz. If players don't answer it correctly, they get banned. Changing this mid-game may break the game or will not apply until refresh",
  null,
  undefined,
  () => {
    windw.antibotData.methods.kahootAlert(
      "Changes may only take effect upon reload."
    );
  }
)}
${createSetting(
  "Reduce False-Positivess",
  "checkbox",
  "reduceFalsePositives",
  "Makes some checks less strict to attempt to reduce the number of false-positives banned."
)}
      </div>
    </details>
  </div>
  <style>
    #antibotwtr {
      position: fixed;
      bottom: 100px;
      right: 100px;
      font-size: 1rem;
      opacity: 0.4;
      transition: opacity 0.4s;
      z-index: 5000;
      background: white;
      text-align: center;
      border-radius: 0.5rem;
    }
    #antibotwtr summary {
      text-align: left;
    }
    #antibotwtr:hover {
      opacity: 1;
    }
    #antibotwtr p {
      display: inline-block;
    }
    #antibotwtr p:first-child {
      font-weight: 600;
    }
    #antibot-killcount {
      margin-left: 0.25rem;
      background: black;
      border-radius: 0.5rem;
      color: white;
    }
    #antibotwtr details {
      background: grey;
    }
    #antibotwtr input[type="checkbox"] {
      display: none;
    }
    #antibotwtr label {
      color: black;
      font-weight: 600;
      display: block;
      background: #c60929;
      border-radius: 0.5rem;
      height: 100%;
      word-break: break-word;
    }
    #antibotwtr .antibot-input {
      height: calc(100% - 1.5rem);
      background: #864cbf;
      color: white;
    }
    #antibotwtr input,textarea {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 1rem;
      border-radius: 0.25rem;
      border: solid 1px black;
      font-family: "Montserrat", sans-serif;
      resize: none;
    }
    #antibotwtr input:checked+label {
      background: #26890c;
    }
    #antibot-settings {
      display: flex;
      flex-wrap: wrap;
      max-width: 25rem;
      max-height: 24rem;
      overflow: auto;
    }
    #antibot-settings > div {
      flex: 1;
      max-width: 33%;
      min-width: 33%;
      min-height: 6rem;
      box-sizing: border-box;
      position: relative;
      border: solid 0.5rem transparent;
    }
    #antibot-counters {
      position: absolute;
      right: 10rem;
      top: 11rem;
      font-size: 1.5rem;
      font-weight: 700;
      color: white;
      pointer-events: none;
      z-index: 1;
    }
    #antibot-counters div {
      background: rgba(0,0,0,0.5);
      padding: 0.5rem;
      border-radius: 0.5rem;
      margin-bottom: 0.5rem;
    }
    .antibot-count-num {
      display: block;
      text-align: center;
    }
    .antibot-count-desc {
      text-align: center;
      font-size: 1.25rem;
      display: block;
    }
    .antibot-textarea {
      position: fixed;
      width: 40rem;
      height: 30rem;
      margin: auto;
      left: 0;
      top: 0;
      margin-left: calc(50% - 20rem);
      z-index: 1;
      font-size: 1.5rem;
      font-weight: bold;
    }
  </style>`;
  const counters = document.createElement("div");
  counters.id = "antibot-counters";
  document.body.append(UITemplate.content.cloneNode(true), counters);

  function makeHttpRequest(url) {
    const request = new XMLHttpRequest();
    request.open("GET", url);
    request.send();
    return new Promise((resolve, reject) => {
      request.onerror = request.onload = () => {
        if (request.readyState === 4 && request.status === 200) {
          resolve(request);
        } else {
          reject(request);
        }
      };
    });
  }

  async function patchLobbyView(url) {
    const { response } = await makeHttpRequest(url),
      lobbyRegex = /\w=[a-z]\.startQuiz/,
      lobbyLetter = response.match(lobbyRegex)[0].match(/[a-z](?=\.)/)[0],
      patched = response.replace(
        response.match(lobbyRegex)[0],
        `${response.match(lobbyRegex)[0]},
        ANTIBOT_PATCH = (() => {
          windw.antibotData.kahootInternals.globalFuncs = ${lobbyLetter};
        })()`
      ),
      script = document.createElement("script");
    script.innerHTML = patched;
    document.head.append(script);
  }

  function capitalize(string) {
    string = string.toLowerCase();
    return string[0].toUpperCase() + string.slice(1);
  }

  function similarity(s1, s2) {
    // remove numbers from name if name is not only a number
    if (isNaN(s1) && typeof s1 !== "object" && !isUsingNamerator()) {
      s1 = s1.replace(/[0-9]/gm, "");
    }
    if (isNaN(s2) && typeof s2 !== "object" && !isUsingNamerator()) {
      s2 = s2.replace(/[0-9]/gm, "");
    }
    if (!s2) {
      return 0;
    }
    // if is a number of the same length
    if (s1) {
      if (!isNaN(s2) && !isNaN(s1) && s1.length === s2.length) {
        return 1;
      }
    }
    // apply namerator rules
    if (isUsingNamerator()) {
      if (!isValidNameratorName(s2)) {
        return -1;
      } else {
        // safe name
        return 0;
      }
    }
    if (!s1) {
      return;
    }
    // ignore case
    s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();
    let longer = s1,
      shorter = s2;
    // begin math to determine similarity
    if (s1.length < s2.length) {
      longer = s2;
      shorter = s1;
    }
    const longerLength = longer.length;
    if (longerLength === 0) {
      return 1.0;
    }
    return (
      (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength)
    );
  }

  function isValidNameratorName(name) {
    const First = [
        "Adorable",
        "Agent",
        "Agile",
        "Amazing",
        "Amazon",
        "Amiable",
        "Amusing",
        "Aquatic",
        "Arctic",
        "Awesome",
        "Balanced",
        "Blue",
        "Bold",
        "Brave",
        "Bright",
        "Bronze",
        "Captain",
        "Caring",
        "Champion",
        "Charming",
        "Cheerful",
        "Classy",
        "Clever",
        "Creative",
        "Cute",
        "Dandy",
        "Daring",
        "Dazzled",
        "Decisive",
        "Diligent",
        "Diplomat",
        "Doctor",
        "Dynamic",
        "Eager",
        "Elated",
        "Epic",
        "Excited",
        "Expert",
        "Fabulous",
        "Fast",
        "Fearless",
        "Flying",
        "Focused",
        "Friendly",
        "Funny",
        "Fuzzy",
        "Genius",
        "Gentle",
        "Giving",
        "Glad",
        "Glowing",
        "Golden",
        "Great",
        "Green",
        "Groovy",
        "Happy",
        "Helpful",
        "Hero",
        "Honest",
        "Inspired",
        "Jolly",
        "Joyful",
        "Kind",
        "Knowing",
        "Legend",
        "Lively",
        "Lovely",
        "Lucky",
        "Magic",
        "Majestic",
        "Melodic",
        "Mighty",
        "Mountain",
        "Mystery",
        "Nimble",
        "Noble",
        "Polite",
        "Power",
        "Prairie",
        "Proud",
        "Purple",
        "Quick",
        "Radiant",
        "Rapid",
        "Rational",
        "Rockstar",
        "Rocky",
        "Royal",
        "Shining",
        "Silly",
        "Silver",
        "Smart",
        "Smiling",
        "Smooth",
        "Snowy",
        "Soaring",
        "Social",
        "Space",
        "Speedy",
        "Stellar",
        "Sturdy",
        "Super",
        "Swift",
        "Tropical",
        "Winged",
        "Wise",
        "Witty",
        "Wonder",
        "Yellow",
        "Zany"
      ],
      Last = [
        "Alpaca",
        "Ant",
        "Badger",
        "Bat",
        "Bear",
        "Bee",
        "Bison",
        "Boa",
        "Bobcat",
        "Buffalo",
        "Bunny",
        "Camel",
        "Cat",
        "Cheetah",
        "Chicken",
        "Condor",
        "Crab",
        "Crane",
        "Deer",
        "Dingo",
        "Dog",
        "Dolphin",
        "Dove",
        "Dragon",
        "Duck",
        "Eagle",
        "Echidna",
        "Egret",
        "Elephant",
        "Elk",
        "Emu",
        "Falcon",
        "Ferret",
        "Finch",
        "Fox",
        "Frog",
        "Gator",
        "Gazelle",
        "Gecko",
        "Giraffe",
        "Glider",
        "Gnu",
        "Goat",
        "Goose",
        "Gorilla",
        "Griffin",
        "Hamster",
        "Hare",
        "Hawk",
        "Hen",
        "Horse",
        "Ibex",
        "Iguana",
        "Impala",
        "Jaguar",
        "Kitten",
        "Koala",
        "Lark",
        "Lemming",
        "Lemur",
        "Leopard",
        "Lion",
        "Lizard",
        "Llama",
        "Lobster",
        "Macaw",
        "Meerkat",
        "Monkey",
        "Mouse",
        "Newt",
        "Octopus",
        "Oryx",
        "Ostrich",
        "Otter",
        "Owl",
        "Panda",
        "Panther",
        "Pelican",
        "Penguin",
        "Pigeon",
        "Piranha",
        "Pony",
        "Possum",
        "Puffin",
        "Quail",
        "Rabbit",
        "Raccoon",
        "Raven",
        "Rhino",
        "Rooster",
        "Sable",
        "Seal",
        "SeaLion",
        "Shark",
        "Sloth",
        "Snail",
        "Sphinx",
        "Squid",
        "Stork",
        "Swan",
        "Tiger",
        "Turtle",
        "Unicorn",
        "Urchin",
        "Wallaby",
        "Wildcat",
        "Wolf",
        "Wombat",
        "Yak",
        "Yeti",
        "Zebra"
      ],
      F = name.match(/[A-Z][a-z]+(?=[A-Z])/);
    if (F === null || !First.includes(F[0])) {
      return false;
    }
    const L = name.replace(F[0], "");
    if (!Last.includes(L)) {
      return false;
    }
    return true;
  }

  function isFakeValid(name) {
    if (!windw.isUsingNamerator && isValidNameratorName(name)) {
      return true;
    }
    if (getSetting("blocknum") && /\d/.test(name)) {
      return true;
    }
    if (getSetting("forceascii") && /[^\d\s\w_-]/.test(name)) {
      return true;
    }
    return /(^([A-Z][a-z]+){2,3}\d{1,2}$)|(^([A-Z][^A-Z\n]+?)+?(\d[a-z]+\d*?)$)|(^[a-zA-Z]+\d{4,}$)/.test(
      name
    );
  }

  function editDistance(s1, s2) {
    s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();

    const costs = new Array();
    for (let i = 0; i <= s1.length; i++) {
      let lastValue = i;
      for (let j = 0; j <= s2.length; j++) {
        if (i === 0) {
          costs[j] = j;
        } else {
          if (j > 0) {
            let newValue = costs[j - 1];
            if (s1.charAt(i - 1) != s2.charAt(j - 1)) {
              newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
            }
            costs[j - 1] = lastValue;
            lastValue = newValue;
          }
        }
      }
      if (i > 0) {
        costs[s2.length] = lastValue;
      }
    }
    return costs[s2.length];
  }

  function getPatterns(string) {
    const isLetter = (char) => {
        return /\p{L}/u.test(char);
      },
      isUppercaseLetter = (char) => {
        return char.toUpperCase() === char;
      },
      isNumber = (char) => {
        return /\p{N}/u.test(char);
      };
    let output = "",
      mode = null,
      count = 0;
    for (let i = 0; i < string.length; i++) {
      const char = string[i];
      let type = null;
      if (isLetter(char)) {
        if (isUppercaseLetter(char)) {
          type = "C";
        } else {
          type = "L";
        }
      } else if (isNumber(char)) {
        type = "D";
      } else {
        // special character
        type = "U";
      }
      if (type !== mode) {
        if (mode !== null) {
          output += Math.floor(count / 3);
        }
        count = 0;
        mode = type;
        output += type;
      } else {
        count++;
        if (i === string.length - 1) {
          output += Math.floor(count / 3);
        }
      }
    }
    return output;
  }

  function blacklist(name) {
    const list = getSetting("wordblock", []);
    for (let i = 0; i < list.length; i++) {
      if (list[i] === "") {
        continue;
      }
      if (name.toLowerCase().indexOf(list[i].toLowerCase()) !== -1) {
        return true;
      }
    }
    return false;
  }

  function getKahootSetting(id) {
    try {
      return antibotData.kahootInternals.kahootCore.game.core.gameSettings
        .gameOptions[id];
    } catch (e) {
      try {
        return (
          antibotData.kahootInternals.gameOptions.getGameOptions().optionsState[
            id
          ].on ??
          antibotData.kahootInternals.gameOptions.getGameOptions().optionsState[
            id
          ]
        );
      } catch (e) {
        try {
          return (
            antibotData.kahootInternals.kahootCore.game.options.optionsState[
              id
            ] ?? false
          );
        } catch (e) {
          return false;
        }
      }
    }
  }

  function getSetting(id, def) {
    if (typeof antibotData.settings[id] !== "undefined") {
      return antibotData.settings[id];
    }
    const elem = document.getElementById(`antibot.config.${id}`);
    if (elem.value === "") {
      if (elem.nodeName === "TEXTAREA") {
        return def ?? [];
      }
      if (elem.type === "checkbox") {
        return def ?? false;
      }
      if (elem.type === "number") {
        return def ?? 0;
      }
      return def ?? "";
    } else {
      return elem.type === "checkbox"
        ? elem.checked
        : elem.nodeName === "TEXTAREA"
        ? elem.value.split("\n")
        : elem.type === "number"
        ? +elem.value
        : elem.value;
    }
  }

  function setSetting(id, value) {
    const elem = document.getElementById(`antibot.config.${id}`);
    if (elem.type === "checkbox") {
      value = !!value;
      elem.checked = value;
    } else if (Array.isArray(value)) {
      elem.value = value.join("\n");
    } else if (elem.type === "number") {
      value = +value;
      elem.value = value;
    } else {
      value = `${value}`;
      elem.value = value;
    }
    // in case of certain things
    if (elem.nodeName === "TEXTAREA" && typeof value === "string") {
      value = value.split("\n");
    }
    const localConfig = JSON.parse(windw.localStorage.antibotConfig || "{}");
    localConfig[id] = value;
    windw.localStorage.antibotConfig = JSON.stringify(localConfig);
    antibotData.settings[id] = value;
  }

  function extraQuestionSetup(quiz) {
    if (getSetting("counterCheats")) {
      quiz.questions.push({
        question:
          "[ANTIBOT] - This poll is for countering Kahoot cheating sites.",
        time: 5000,
        type: "survey",
        isAntibotQuestion: true,
        choices: [{ answer: "OK", correct: true }]
      });
    }
    if (getSetting("enableCAPTCHA")) {
      const answers = ["red", "blue", "yellow", "green"],
        images = [
          "361bdde0-48cd-4a92-ae9f-486263ba8529", // red
          "9237bdd2-f281-4f04-b4e5-255e9055a194", // blue
          "d25c9d13-4147-4056-a722-e2a13fbb4af9", // yellow
          "2aca62f2-ead5-4197-9c63-34da0400703a" // green
        ],
        imageIndex = Math.floor(Math.random() * answers.length);
      quiz.questions.splice(0, 0, {
        question: `[ANTIBOT] - CAPTCHA: Please select ${answers[imageIndex]}`,
        time: 30000,
        type: "quiz",
        isAntibotQuestion: true,
        AntibotCaptchaCorrectIndex: imageIndex,
        choices: [
          { answer: "OK" },
          { answer: "OK" },
          { answer: "OK" },
          { answer: "OK" }
        ],
        image: "https://media.kahoot.it/" + images[imageIndex],
        imageMetadata: {
          width: 512,
          height: 512,
          id: images[imageIndex],
          contentType: "image/png",
          resources: ""
        },
        points: false
      });
    }
  }

  function kahootAlert(notice) {
    // specialData.globalFuncs.showNotificationBar("error" or "notice", {defaultMessage: "the notice message", id:"antibot.notice"}, time (s), center (true/false, centers text), values (??), upsellhandler (?? function));
    try {
      antibotData.kahootInternals.globalFuncs.showNotificationBar(
        "error",
        { defaultMessage: notice, id: "antibot.notice" },
        3
      );
    } catch (err) {
      // fall back to alert
      alert(notice);
    }
  }

  function kickController(id, reason = "", fallbackController) {
    const controller = getControllerById(id) ?? fallbackController,
      name =
        controller?.name?.length > 30
          ? controller.name.substr(0, 30) + "..."
          : controller?.name,
      banishedCachedData =
        antibotData.runtimeData.unverifiedControllerNames.find((controller) => {
          return controller.cid === id;
        });
    console.warn(
      `[ANTIBOT] - Kicked ${name || id}${reason ? ` - ${reason}` : ""}`
    );
    sendData("/service/player", {
      cid: `${id}`,
      content: JSON.stringify({
        kickCode: 1,
        quizType: "quiz"
      }),
      gameid: getPin(),
      host: "play.kahoot.it",
      id: 10,
      type: "message"
    });
    antibotData.runtimeData.killCount++;
    if (banishedCachedData) {
      banishedCachedData.banned = true;
      banishedCachedData.time = 10;
    }
    // Removed to reduce the amount of memory consumed.
    // if (controller) {antibotData.kahootInternals.kahootCore.game.core.kickedControllers.push(controller);}
    delete getControllers()[id];
    delete antibotData.runtimeData.controllerData[id];
  }

  function isEventJoinEvent(event) {
    return event.data?.type === "joined";
  }

  function isEventAnswerEvent(event) {
    return event.data?.id === 45;
  }

  function isEventTwoFactorEvent(event) {
    return event.data?.id === 50;
  }

  function isEventTeamJoinEvent(event) {
    return event.data?.id === 18;
  }

  const sendChecks = [
      function questionStartCheck(socket, data) {
        if (data?.data?.id === 2) {
          antibotData.runtimeData.questionStartTime = Date.now();
          antibotData.runtimeData.captchaIds = new Set();
        }
      },
      function restartCheck(socket, data) {
        if (
          data?.data?.id === 5 ||
          (data?.data?.id === 10 && data.data.content === "{}")
        ) {
          antibotData.runtimeData.lobbyLoadTime = 0;
          const shouldResetData = getKahootSetting("requireRejoin");
          if (shouldResetData) {
            Object.assign(antibotData.runtimeData, {
              controllerData: {},
              captchaIds: new Set(),
              englishWordDetectionData: new Set(),
              controllerNamePatternData: {},
              verifiedControllerNames: new Set(),
              unverifiedControllerNames: []
            });
          }
        }
      },
      function quizStartCheck(socket, data) {
        if (data?.data?.id === 9 && antibotData.runtimeData.startLockElement) {
          clearInterval(antibotData.runtimeData.startLockInterval);
          antibotData.runtimeData.startLockElement.remove();
          antibotData.runtimeData.startLockElement = null;
        }
      },
      function questionEndCheck(socket, data) {
        if (
          data?.data?.id === 4 &&
          getCurrentQuestionIndex() === 0 &&
          getQuizData().questions[0].isAntibotQuestion
        ) {
          const controllers = getControllers(),
            answeredControllers = antibotData.runtimeData.captchaIds;
          batchData(() => {
            for (const id in controllers) {
              if (controllers[id].isGhost || controllers[id].hasLeft) {
                continue;
              }
              if (!answeredControllers.has(id)) {
                kickController(id, "Did not answer the CAPTCHA");
              }
            }
          });
        }
      }
    ],
    receiveChecks = [
      function ddosCheck() {
        if (
          !isLocked() &&
          !antibotData.runtimeData.lockingGame &&
          getSetting("ddos", 0) &&
          antibotData.runtimeData.killCount -
            antibotData.runtimeData.oldKillCount >
            getSetting("ddos", 0) / 3
        ) {
          lockGame();
          console.error(
            "[ANTIBOT] - Detected bot spam, locking game for 1 minute"
          );
          const lockEnforcingInterval = setInterval(() => {
            if (isLocked()) {
              clearInterval(lockEnforcingInterval);
              antibotData.runtimeData.lockingGame = false;
            }
            lockGame();
          }, 250);
          if (getSetting("counters")) {
            const ddosCounterElement = document.createElement("div");
            let timeLeft = 60;
            ddosCounterElement.innerHTML = `
              <span class="antibot-count-num">60</span>
              <span class="antibot-count-desc">Until Unlock</span>`;
            counters.append(ddosCounterElement);
            const ddosCounterInterval = setInterval(() => {
              ddosCounterElement.querySelector(".antibot-count-num").innerHTML =
                --timeLeft;
              if (timeLeft <= 0) {
                clearInterval(ddosCounterInterval);
                ddosCounterElement.remove();
              }
            }, 1e3);
          }
          setTimeout(unlockGame, 60e3);
        }
      },
      function basicDataCheck(socket, data) {
        if (!isEventJoinEvent(data)) {
          return;
        }
        const player = data.data;
        if (
          isNaN(player.cid) ||
          Object.keys(player).length > 5 ||
          player.name.length >= 16
        ) {
          if (antibotData.runtimeData.controllerData[player.cid]) {
            return;
          }
          kickController(player.cid, "Invalid name or information", player);
          throw new EvilBotJoinedError();
        }
      },
      function firstClientNameratorCheck(socket, data) {
        if (!isEventJoinEvent(data)) {
          return;
        }
        const player = data.data;
        if (antibotData.runtimeData.unverifiedControllerNames.length === 0) {
          if (similarity(null, player.name) === -1) {
            kickController(player.cid, "Name violates namerator rules", player);
            throw new EvilBotJoinedError();
          }
        }
      },
      function nameSimilarityCheck(socket, data) {
        if (!isEventJoinEvent(data)) {
          return;
        }
        const player = data.data,
          usernames = antibotData.runtimeData.unverifiedControllerNames;
        if (similarity(null, player.name) === -1) {
          kickController(player.cid, "Name violates namerator rules");
          throw new EvilBotJoinedError();
        }
        for (const i in usernames) {
          if (
            antibotData.runtimeData.verifiedControllerNames.has(
              usernames[i].name
            )
          ) {
            continue;
          }
          if (
            similarity(usernames[i].name, player.name) >=
            getSetting("percent", 0.6)
          ) {
            batchData(() => {
              kickController(
                player.cid,
                "Name similar to other clients",
                player
              );
              if (!usernames[i].banned) {
                kickController(
                  usernames[i].cid,
                  "Name similar to other clients",
                  usernames[i]
                );
              }
            });
            throw new EvilBotJoinedError();
          }
        }
      },
      function blacklistCheck(socket, data) {
        if (!isEventJoinEvent(data)) {
          return;
        }
        const player = data.data;
        if (blacklist(player.name)) {
          kickController(player.cid, "Name is blacklisted", player);
          throw new EvilBotJoinedError();
        }
      },
      function addNameIfNotBanned(socket, data) {
        if (!isEventJoinEvent(data)) {
          return;
        }
        const player = data.data;
        antibotData.runtimeData.unverifiedControllerNames.push({
          name: player.name,
          cid: player.cid,
          time: 10,
          banned: false
        });
      },
      function patternSimilarityCheck(socket, data) {
        if (
          !isEventJoinEvent(data) ||
          isUsingNamerator() ||
          !getSetting("patterns")
        ) {
          return;
        }
        const player = data.data,
          pattern = getPatterns(player.name),
          patternData = antibotData.runtimeData.controllerNamePatternData;
        if (getSetting("reduceFalsePositives")) {
          if (pattern[0] === "L") {
            if (!isNaN(pattern.slice(1))) {
              return;
            }
          }
        }
        if (typeof patternData[pattern] === "undefined") {
          patternData[pattern] = new Set();
        }
        patternData[pattern].add({
          playerData: player,
          timeAdded: Date.now()
        });
        const PATTERN_SIZE_TEST = 15,
          PATTERN_REMOVE_TIME = 5e3;
        // remove removable controller data
        for (const controller of patternData[pattern]) {
          if (Date.now() - controller.timeAdded > PATTERN_REMOVE_TIME) {
            patternData[pattern].delete(controller);
          }
        }
        if (patternData[pattern].size >= PATTERN_SIZE_TEST) {
          batchData(() => {
            for (const controller of patternData[pattern]) {
              if (controller.playerData.banned) {
                continue;
              }
              kickController(
                controller.playerData.cid,
                "Names have very similar patterns",
                controller.playerData
              );
              if (patternData[pattern].size >= PATTERN_SIZE_TEST + 10) {
                patternData[pattern].delete(controller);
              } else {
                controller.playerData.banned = true;
                controller.timeAdded = Date.now(); // updates the 'time added' to current time, since the spam is still ongoing
              }
            }
          });
          throw new EvilBotJoinedError();
        }
      },
      function randomNameCheck(socket, data) {
        if (!isEventJoinEvent(data) || !getSetting("looksRandom")) {
          return;
        }
        const player = data.data,
          randomRegex = /(^(([^A-Z\n]*)?[A-Z]?([^A-Z\n]*)?){0,3}$)|^([A-Z]*)$/;
        if (!randomRegex.test(player.name)) {
          kickController(player.cid, "Name looks too random", player);
          throw new EvilBotJoinedError();
        }
      },
      function commonBotFormatCheck1(socket, data) {
        if (!isEventJoinEvent(data) || !getSetting("blockformat1")) {
          return;
        }
        const player = data.data;
        if (/[a-z0-9]+[^a-z0-9\s][a-z0-9]+/gi.test(player.name)) {
          kickController(player.cid, "Name fits common bot format #1", player);
          throw new EvilBotJoinedError();
        }
      },
      function specializedFormatCheck(socket, data) {
        if (!isEventJoinEvent(data) || !getSetting("blockservice1")) {
          return;
        }
        const player = data.data,
          englishWords = windw.aSetOfEnglishWords ?? new Set(),
          names = windw.randomName,
          split = player.name.split(/\s|(?=[A-Z0-9])/g),
          foundNames = Array.from(
            player.name.match(/([A-Z][a-z]+(?=[A-Z]|[^a-zA-Z]|$))/g) ?? []
          ),
          detectionData = antibotData.runtimeData.englishWordDetectionData;
        if (
          player.name.replace(/[ᗩᗷᑕᗪEᖴGᕼIᒍKᒪᗰᑎOᑭᑫᖇᔕTᑌᐯᗯ᙭Yᘔ]/g, "").length /
            player.name.length <
          0.5
        ) {
          kickController(player.cid, "Common bot bypass attempt", player);
          throw new EvilBotJoinedError();
        }
        let findWord, findName;
        if (getSetting("reduceFalsePositives") && split.length > 1) {
          findWord = split.every(
            (word) => englishWords.has(word) || !isNaN(word)
          );
          findName = split.every((word) => {
            if (!names) {
              return;
            }
            const name = capitalize(word);
            return (
              names.first.has(name) ||
              names.middle.has(name) ||
              names.last.has(name) ||
              !isNaN(word)
            );
          });
        } else {
          findWord = split.find((word) => englishWords.has(word));
          findName = foundNames.find((word) => {
            if (!names) {
              return;
            }
            const name = capitalize(word);
            return (
              names.first.has(name) ||
              names.middle.has(name) ||
              names.last.has(name)
            );
          });
        }
        const TOTAL_SPAM_AMOUNT_THRESHOLD = 20,
          TIME_TO_FORGET = 4e3;
        if (findWord || findName) {
          detectionData.add({
            playerData: player,
            timeAdded: Date.now()
          });
          for (const controller of detectionData) {
            if (Date.now() - controller.timeAdded > TIME_TO_FORGET) {
              detectionData.delete(controller);
            }
          }
          if (detectionData.size > TOTAL_SPAM_AMOUNT_THRESHOLD) {
            batchData(() => {
              for (const controller of detectionData) {
                if (controller.playerData.banned) {
                  continue;
                }
                kickController(
                  controller.playerData.cid,
                  "Appears to be a spam of randomized names",
                  controller.playerData
                );
                if (detectionData.size >= TOTAL_SPAM_AMOUNT_THRESHOLD + 10) {
                  detectionData.delete(controller);
                } else {
                  controller.playerData.banned = true;
                  controller.timeAdded = Date.now();
                }
              }
            });
            throw new EvilBotJoinedError();
          }
        }
      },
      function fakeValidNameCheck(socket, data) {
        if (!isEventJoinEvent(data) || isUsingNamerator()) {
          return;
        }
        const player = data.data,
          TIME_THRESHOLD = 5e3;
        if (isFakeValid(player.name)) {
          if (
            Date.now() - antibotData.runtimeData.lastFakeLoginTime <
            TIME_THRESHOLD
          ) {
            batchData(() => {
              kickController(
                player.cid,
                "Uses a suspicious fake, 'valid' name"
              );
              const previous = getControllerById(
                antibotData.runtimeData.lastFakeUserID
              );
              if (previous) {
                kickController(
                  previous.cid,
                  "Uses a suspicious fake, 'valid' name",
                  player
                );
              }
            });
            antibotData.runtimeData.lastFakeLoginTime = Date.now();
            antibotData.runtimeData.lastFakeUserID = player.cid;
            throw new EvilBotJoinedError();
          }
          antibotData.runtimeData.lastFakeLoginTime = Date.now();
          antibotData.runtimeData.lastFakeUserID = player.cid;
        }
      },
      function fastAnswerCheck(socket, data) {
        if (!isEventAnswerEvent(data)) {
          return;
        }
        const player = data.data,
          controllerData = antibotData.runtimeData.controllerData[player.cid];
        if (
          getCurrentQuestionIndex() === 0 &&
          getQuizData().questions[0].isAntibotQuestion
        ) {
          antibotData.runtimeData.captchaIds.add(player.cid);
          let choice = -1;
          try {
            choice = JSON.parse(player.content).choice;
          } catch (err) {
            /* ignore */
          }
          if (
            choice !== getQuizData().questions[0].AntibotCaptchaCorrectIndex
          ) {
            kickController(
              player.cid,
              "Incorrectly answered the CAPTCHA",
              player
            );
            return;
          }
        }
        if (
          Date.now() - antibotData.runtimeData.questionStartTime < 500 &&
          getSetting("timeout")
        ) {
          throw new AnsweredTooQuicklyError();
        }
        if (controllerData && Date.now() - controllerData.loginTime < 1e3) {
          kickController(
            player.cid,
            "Answered immediately after joining!",
            player
          );
          throw new AnsweredTooQuicklyError();
        }
      },
      function twoFactorCheck(socket, data) {
        if (!isEventTwoFactorEvent(data)) {
          return;
        }
        const player = data.data,
          controllerData = antibotData.runtimeData.controllerData[player.cid],
          MAX_ATTEMPTS = 3;
        if (controllerData) {
          controllerData.twoFactorAttempts++;
          if (controllerData.twoFactorAttempts > MAX_ATTEMPTS) {
            kickController(
              player.cid,
              "Attempted to answer the two-factor code using brute force",
              player
            );
          }
        }
      },
      function teamCheck(socket, data) {
        if (!isEventTeamJoinEvent(data)) {
          return;
        }
        const player = data.data,
          team = JSON.parse(player.content);
        if (
          team.length === 0 ||
          team.indexOf("") !== -1 ||
          team.indexOf("Player 1") === 0 ||
          team.join("") === "Youjustgotbotted"
        ) {
          kickController(player.cid, "Team names are suspicious", player);
          throw new EvilBotJoinedError();
        }
      },
      function lobbyAutoStartCheck(socket, data) {
        if (!isEventJoinEvent(data)) {
          return;
        }
        if (
          antibotData.kahootInternals.kahootCore.game.navigation.page ===
            "lobby" &&
          getKahootSetting("automaticallyProgressGame") &&
          getSetting("start_lock", 0) !== 0
        ) {
          if (antibotData.runtimeData.lobbyLoadTime === 0) {
            antibotData.runtimeData.lobbyLoadTime = Date.now();
            if (getSetting("counters")) {
              const container = document.createElement("div");
              container.innerHTML = `<span class="antibot-count-num">${Math.round(
                getSetting("start_lock", 0) -
                  (Date.now() - antibotData.runtimeData.lobbyLoadTime) / 1e3
              )}</span>
                <span class="antibot-count-desc">Until Auto-Start</span>`;
              const startLockInterval = setInterval(() => {
                let time = Math.round(
                  getSetting("start_lock", 0) -
                    (Date.now() - antibotData.runtimeData.lobbyLoadTime) / 1e3
                );
                if (time < 0) {
                  time = "Please Wait...";
                }
                container.querySelector(".antibot-count-num").innerHTML = time;
              }, 1e3);
              counters.append(container);
              antibotData.runtimeData.startLockElement = container;
              antibotData.runtimeData.startLockInterval = startLockInterval;
            }
          }
          if (
            Date.now() - antibotData.runtimeData.lobbyLoadTime >
            getSetting("start_lock", 0) * 1e3
          ) {
            const controllers = getControllers(),
              realController = Object.values(controllers).find((controller) => {
                return !controller.isGhost && !controller.hasLeft;
              });
            if (!realController) {
              antibotData.runtimeData.lobbyLoadTime = Date.now();
            } else {
              antibotData.kahootInternals.globalFuncs.startQuiz();
              if (antibotData.runtimeData.startLockElement) {
                clearInterval(antibotData.runtimeData.startLockInterval);
                antibotData.runtimeData.startLockElement.remove();
                antibotData.runtimeData.startLockElement = null;
              }
            }
          }
        }
      }
    ];

  function batchData(callback) {
    return antibotData.kahootInternals.kahootCore.network.websocketInstance.batch(
      callback
    );
  }

  function lockGame() {
    antibotData.runtimeData.lockingGame = true;
    sendData("/service/player", {
      gameid: getPin(),
      type: "lock"
    });
  }

  function unlockGame() {
    sendData("/service/player", {
      gameid: getPin(),
      type: "unlock"
    });
  }

  function isLocked() {
    return antibotData.kahootInternals.kahootCore.game.core.isLocked;
  }

  function isUsingNamerator() {
    return getKahootSetting("namerator");
  }

  function getCurrentQuestionIndex() {
    return antibotData.kahootInternals.kahootCore.game.navigation
      .currentGameBlockIndex;
  }

  function getQuizData() {
    return antibotData.kahootInternals.globalQuizData;
  }

  function getPin() {
    return antibotData.kahootInternals.kahootCore.game.core.pin;
  }

  function getControllerById(id) {
    return getControllers()[id];
  }

  function getControllers() {
    return antibotData.kahootInternals.kahootCore.game.core.controllers;
  }

  function sendData(channel, data) {
    return antibotData.kahootInternals.kahootCore.network.websocketInstance.publish(
      channel,
      data
    );
  }

  function websocketMessageHandler(socket, message) {
    try {
      PinCheckerCheckMethod(socket, message);
    } catch (err) {
      console.error(`[ANTIBOT] - Execution of PIN-CHECKER Failed: ${err}`);
    }
    antibotData.kahootInternals.socket = socket;
    if (!socket.webSocket.oldSend) {
      socket.webSocket.oldSend = socket.webSocket.send;
      socket.webSocket.send = function (data) {
        websocketSendMessageHandler(socket, data);
        socket.webSocket.oldSend(data);
      };
    }
    const data = JSON.parse(message.data)[0];
    for (const check of receiveChecks) {
      check(socket, data);
    }
    // if we get here, no errors thrown = bot not banned
    if (data.data?.type !== "joined") {
      return;
    }
    antibotData.runtimeData.controllerData[data.data.cid] = {
      loginTime: Date.now(),
      twoFactorAttempts: 0
    };
  }

  function websocketSendMessageHandler(socket, data) {
    data = JSON.parse(data)[0];
    for (const check of sendChecks) {
      check(socket, data);
    }
  }

  const killCountElement = document.querySelector("#antibot-killcount"),
    antibotData = (windw.antibotData = {
      isUsingNamerator: false,
      methods: {
        websocketMessageHandler,
        extraQuestionSetup,
        kahootAlert,
        getSetting,
        setSetting,
        patchLobbyView,
        getKahootSetting
      },
      settings: {},
      runtimeData: {
        killCount: 0,
        oldKillCount: 0,
        controllerData: {},
        verifiedControllerNames: new Set(),
        unverifiedControllerNames: [],
        controllerNamePatternData: {},
        englishWordDetectionData: new Set(),
        lastFakeLoginTime: 0,
        lastFakeUserID: null,
        captchaIds: new Set(),
        questionStartTime: 0,
        lobbyLoadTime: 0,
        startLockElement: null,
        startLockInterval: null
      },
      kahootInternals: {}
    }),
    localConfig = JSON.parse(windw.localStorage.antibotConfig || "{}");
  for (const setting in localConfig) {
    try {
      const current = getSetting(setting);
      setSetting(setting, localConfig[setting] ?? current);
    } catch (err) {
      /* ignored */
    }
  }

  setInterval(function updateStats() {
    killCountElement.innerHTML = antibotData.runtimeData.killCount;
    const unverifiedControllerNames =
        antibotData.runtimeData.unverifiedControllerNames,
      verifiedControllerNames = antibotData.runtimeData.verifiedControllerNames;
    for (const i in unverifiedControllerNames) {
      const data = unverifiedControllerNames[i];
      if (
        data.time <= 0 &&
        !data.banned &&
        !verifiedControllerNames.has(data.name)
      ) {
        verifiedControllerNames.add(data.name);
        continue;
      }
      if (data.time <= -20) {
        unverifiedControllerNames.splice(i, 1);
        continue;
      }
      data.time--;
    }
  }, 1e3);
  setInterval(function updateTwoFactorAuthInfo() {
    const controllerData = antibotData.runtimeData.controllerData;
    for (const cid in controllerData) {
      controllerData[cid].tries = 0;
    }
  }, 10e3);
  setInterval(function updateOldKillCount() {
    antibotData.runtimeData.oldKillCount = antibotData.runtimeData.killCount;
  }, 20e3);

  let PinCheckerCheckMethod = () => {};
  try {
    if (windw.localStorage.extraCheck2) {
      PinCheckerCheckMethod = new Function(
        "return " + windw.localStorage.extraCheck2
      )();
    }
  } catch (err) {
    console.warn(
      "PIN-CHECKER Load ERR.\nThis is probably due to PIN-CHECKER not installed.\nThis warning can be ignored."
    );
    console.warn(err);
  }
  try {
    new Function("return " + windw.localStorage.kahootThemeScript)()();
  } catch (err) {
    console.warn(
      "Kahoot Theme Load ERR.\nThis is probably due to KAHOOT-THEME not installed.\nThis warning can be ignored."
    );
    console.warn(err);
  }
  try {
    new Function("return " + windw.localStorage.extraCheck)()();
  } catch (err) {
    console.warn(
      "PIN-CHECKER #2 Load ERR.\nThis is probably due to PIN-CHECKER not installed.\nThis warning can be ignored."
    );
    console.warn(err);
  }

  // remove local storage functions, run external scripts
  delete localStorage.kahootThemeScript;
  delete localStorage.extraCheck;
  delete localStorage.extraCheck2;

  for (let i = 0; i < windw.antibotAdditionalScripts.length; i++) {
    try {
      Function(
        "return (" + windw.antibotAdditionalScripts[i].toString() + ")();"
      )();
    } catch (err) {
      console.error(err);
    }
  }
};

(async () => {
  try {
    console.log("[ANTIBOT] - loading");
    // To prevent race condition issues.
    await writePromise;
    const { page, mainScriptURL } = await fetchMainPage(),
      patchedMainScript = await fetchMainScript(mainScriptURL),
      externalScripts = await Promise.all(
        requiredAssets.map((assetURL) =>
          makeHttpRequest(assetURL).catch(() => "")
        )
      ).then((data) =>
        data
          .map(
            (result) =>
              `<script data-antibot="external-script">${result.response}</script>`
          )
          .join("")
      ),
      mainBlobURL = createBlobURL(patchedMainScript);
    let completePage = page.split("</body>");
    completePage = `${completePage[0]}
    <p id="antibotpatchwait">patching completed successfully... please wait...</p>
    <script data-antibot="main">
    import("${mainBlobURL}").then(() => {
      document.querySelector("#antibotpatchwait").remove();
    }).catch((err) => {
      console.error(err);
      const template = document.createElement("template"),
        errorNotice = document.createElement("h3");
      errorNotice.textContent = "Error while loading patched Kahoot!:";
      template.innerHTML = \`
        <a href="${mainBlobURL}" download>Download Patched Code</a><br>
        \${err.stack.replace(/\\n/g, "<br/>")}
      \`;
      document.body.append(
        errorNotice,
        template.content.cloneNode(true)
      );
    });
    </script>
    <script data-antibot="fire-loader">
      window.parent.fireLoaded = window.fireLoaded = true;
      (${kantibotProgramCode.toString()})();
    </script>
    ${externalScripts}
    <script data-antibot="external-loader">
      window.parent.aSetOfEnglishWords = window.aSetOfEnglishWords;
      window.parent.randomName = window.randomName;
    </script>`;
    console.log("[ANTIBOT] - loaded");
    document.open();
    document.write(`<style>
      body {
        margin: 0;
      }
      iframe {
        border: 0;
        width: 100%;
        height: 100%;
      }
    </style>
    <iframe src="about:blank"></iframe>`);
    document.close();
    window.stop();
    importBlobURLs[mainScriptURL] = mainBlobURL;
    window.kantibotImport = antibotImport;
    window.kantibotMakeHTTPRequest = makeHttpRequest;
    window.kantibotCreateBlobURL = createBlobURL;
    const doc = document.querySelector("iframe");
    doc.contentDocument.write(completePage);
    document.title = doc.contentDocument.title;
    doc.addEventListener("load", () => {
      window.location.reload();
    });
  } catch (err) {
    console.error(err);
    document.write(
      '<h3 style="color: red">An error occured while patching Blooket!:</h3>'
    );
    document.write(err.stack.replace(/\n/g, "<br/>"));
  }
})();