Kahoot PIN Checker

Check the pin of a kahoot game.

当前为 2020-11-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         Kahoot PIN Checker
// @namespace    http://tampermonkey.net/
// @version      1.0.5
// @description  Check the pin of a kahoot game.
// @author       theusaf
// @match        *://play.kahoot.it/*
// @exclude      *://play.kahoot.it/v2/assets/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

if(window.fireLoaded || (window.parent && window.parent.PinCheckerMain)){
  throw "[PIN-CHECKER] - Already loaded.";
}
console.log("[PIN-CHECKER] - Looking for AntiBot");

/**
 * PinCheckerMain - The main pin checking function
 */
window.PinCheckerMain = function(){

  const windw = window.parent;

  const loader = setInterval(()=>{
    if(!document.querySelector("[data-functional-selector=launch-team-mode-button]")){
      return;
    }
    console.log("[PIN-CHECKER] - Ready!");
    clearInterval(loader);
    document.querySelector("[data-functional-selector=launch-team-mode-button]").addEventListener("click",()=>{
      console.log("[PIN-CHECKER] - Using Team Mode.")
      windw.localStorage.PinCheckerMode = "team";
    });

    if(windw.localStorage.PinCheckerAutoRelogin == "true"){
      const waiter = setInterval(()=>{
        let a = document.querySelector("[data-functional-selector=launch-button]");
        if(windw.localStorage.PinCheckerMode == "team"){
          a = document.querySelector("[data-functional-selector=launch-team-mode-button]");
        }
        delete windw.localStorage.PinCheckerMode;
        if(a && !a.disabled){
          a.click();
          windw.localStorage.PinCheckerAutoRelogin = false;
          clearInterval(waiter);
        }
      },500);
    }else{
      delete windw.localStorage.PinCheckerMode;
    }
  },500);

  windw.PinCheckerNameList = [];
  windw.PinCheckerPin = null;

  /**
   * ResetGame - Reloads the page
   */
  function ResetGame(){
    console.error("[PIN-CHECKER] - Pin Broken. Attempting restart.");
    windw.localStorage.PinCheckerAutoRelogin = true;
    windw.document.write("<scr" + "ipt>" + `window.location = "https://play.kahoot.it/v2/${windw.location.search}";` + "</scr" + "ipt>");
  }

  /**
   * concatTokens - From kahoot.js.org. Combines the tokens.
   *
   * @param  {String} headerToken    decoded token
   * @param  {String} challengeToken decoded token 2
   * @returns {String}               The final token
   */
  function concatTokens(headerToken, challengeToken) {
    // Combine the session token and the challenge token together to get the string needed to connect to the websocket endpoint
    for (var token = "", i = 0; i < headerToken.length; i++) {
        var char = headerToken.charCodeAt(i);
        var mod = challengeToken.charCodeAt(i % challengeToken.length);
        var decodedChar = char ^ mod;
        token += String.fromCharCode(decodedChar);
    }
    return token;
  }

  /**
   * CreateClient - Creates a Kahoot! client to join a game
   * This really only works because kahoot treats kahoot.it, play.kahoot.it, etc as the same thing.
   *
   * @param  {Number} pin The gameid
   */
  function CreateClient(pin){
    console.log("[PIN-CHECKER] - Creating client");
    pin = pin + "";
    const SessionGetter = new XMLHttpRequest();
    SessionGetter.open("GET","/reserve/session/" + pin);
    SessionGetter.send();
    SessionGetter.onload = function(){
      let SessionData;
      try{
        SessionData = JSON.parse(SessionGetter.responseText);
      }catch(e){
        // probably not found
        return ResetGame();
      }
      const TokenHeader = atob(SessionGetter.getResponseHeader("x-kahoot-session-token"));
      let {challenge} = SessionData;
      challenge = challenge.replace(/(\u0009|\u2003)/mg, "");
      challenge = challenge.replace(/this /mg, "this");
      challenge = challenge.replace(/ *\. */mg, ".");
      challenge = challenge.replace(/ *\( */mg, "(");
      challenge = challenge.replace(/ *\) */mg, ")");
      challenge = challenge.replace("console.", "");
      challenge = challenge.replace("this.angular.isObject(offset)", "true");
      challenge = challenge.replace("this.angular.isString(offset)", "true");
      challenge = challenge.replace("this.angular.isDate(offset)", "true");
      challenge = challenge.replace("this.angular.isArray(offset)", "true");
      const merger = "var _ = {" +
          "    replace: function() {" +
          "        var args = arguments;" +
          "        var str = arguments[0];" +
          "        return str.replace(args[1], args[2]);" +
          "    }" +
          "}; " +
          "var log = function(){};" +
          "return ";
      const solver = Function(merger + challenge);
      const ChallengeHeader = solver();
      const FinalToken = concatTokens(TokenHeader,ChallengeHeader);
      const connection = new WebSocket("wss://play.kahoot.it/cometd/" + pin + "/" + FinalToken);
      connection.addEventListener("open",()=>{
        connection.send(JSON.stringify([
          {
            advice: {
              interval: 0,
              timeout: 60000
            },
            minimumVersion: "1.0",
            version: "1.0",
            supportedConnectionTypes: ["websocket","long-polling"],
            channel: "/meta/handshake",
            ext: {
              ack: true,
              timesync: {
                l: 0,
                o: 0,
                tc: Date.now()
              }
            },
            id: 1
          }
        ]));
      });
      let shoken = false,
        timesync = {},
        clientId = "",
        mid = 2,
        closed = false,
        cid = "",
        name = "";
      connection.addEventListener("message",(m)=>{
        const {data} = m;
        const [message] = JSON.parse(data);
        if(message.channel === "/meta/handshake" && !shoken){
          if(message.ext && message.ext.timesync){
            shoken = true;
            clientId = message.clientId;
            const {tc,ts,p} = message.ext.timesync,
              l = Math.round((Date.now() - tc - p) / 2),
              o = ts - tc - l;
            Object.assign(timesync,{
              l,
              o,
              get tc(){
                return Date.now();
              }
            });
            connection.send(JSON.stringify([{
              advice: {timeout:0},
              channel: "/meta/connect",
              id: 2,
              ext: {
                ack: 0,
                timesync
              },
              clientId
            }]));
            // start joining
            setTimeout(()=>{
              name = "KCP_" + (Date.now() + "").substr(2);
              connection.send(JSON.stringify([{
                clientId,
                channel: "/service/controller",
                id: ++mid,
                ext: {},
                data: {
                  gameid: pin,
                  host: "play.kahoot.it",
                  content: JSON.stringify({
                    device: {
                      userAgent: windw.navigator.userAgent,
                      screen: {
                        width: windw.screen.width,
                        height: windw.screen.height
                      }
                    }
                  }),
                  name,
                  type: "login"
                }
              }]));
            },1000);
          }
        }else if(message.channel === "/meta/connect" && shoken && !closed){
          connection.send(JSON.stringify([{
            channel: "/meta/connect",
            id: ++mid,
            ext: {
              ack: message.ext.ack,
              timesync
            },
            clientId
          }]));
        }else if(message.channel === "/service/controller"){
          if(message.data && message.data.type === "loginResponse"){
            if(message.data.error === "NONEXISTING_SESSION"){
              // session doesn't exist
              connection.send(JSON.stringify([{
                channel: "/meta/disconnect",
                clientId,
                id: ++mid,
                ext: {
                  timesync
                }
              }]));
              connection.close();
              ResetGame();
            }else{
              // Check if the client is in the game after 10 seconds
              setTimeout(()=>{
                if(!windw.PinCheckerNameList.includes(name)){
                  // Uh oh! the client didn't join!
                  ResetGame();
                }
              },10e3)
              // good. leave the game.
              connection.send(JSON.stringify([{
                channel: "/meta/disconnect",
                clientId,
                id: ++mid,
                ext: {
                  timesync
                }
              }]));
              closed = true;
              setTimeout(()=>{
                connection.close();
              },500);
            }
          }
        }else if(message.channel === "/service/status"){
          if(message.data.status === "LOCKED"){
            // locked, cannot test
            console.log("[PIN-CHECKER] - Game is locked. Unable to test.");
            closed = true;
            connection.send(JSON.stringify([{
              channel: "/meta/disconnect",
              clientId,
              id: ++mid,
              ext: {
                timesync
              }
            }]));
            setTimeout(()=>{
              connection.close();
            },500);
          }
        }
      });
    };
  }

  windw.PinCheckerInterval = setInterval(()=>{
    if(windw.PinCheckerPin){
      CreateClient(windw.PinCheckerPin);
    }
  },60*1000);
};

/**
 * PinCheckerInjector - Checks messages and stores the names of players who joined within the last few seconds
 *
 * @param  {String} message The websocket message
 */
window.PinCheckerInjector = function(socket,message){
  const windw = window.parent;
  const data = JSON.parse(message.data)[0];
  try{
    const part = document.querySelector("[data-functional-selector=\"game-pin\"]") || document.querySelector("[data-functional-selector=\"bottom-bar-game-pin\"]");
    if(Number(part.innerText) != windw.PinCheckerPin && (Number(part.innerText) != 0) && !isNaN(Number(part.innerText))){
      windw.PinCheckerPin = Number(part.innerText);
      console.log("[PIN-CHECKER] - Discovered new PIN: " + windw.PinCheckerPin);
    }else if(Number(part.innerText) == 0 || isNaN(Number(part.innerText))){
      windw.PinCheckerPin = null;
      console.log("[PIN-CHECKER] - PIN is hidden or game is locked. Unable to test.");
    }
  }catch(err){
    console.warn("[PIN-CHECKER] - Failed to get pin. Full error log below:\n" + err);
  }
  if(data.data && data.data.type === "joined"){
    windw.PinCheckerNameList.push(data.data.name);
    setTimeout(()=>{
      windw.PinCheckerNameList.splice(0,1);
    },20e3);
  }
};

if(!window.page){
  document.write("[PIN-CHECKER] - Patching Kahoot. Please wait. If this screen stays blank for long periods of time, please force reload or clear your cache.");
  let page = new XMLHttpRequest();
  page.open("GET",location.href);
  page.send();
  page.onload = function(){
    let scriptURL = page.response.match(/><\/script><script\ .*?vendors.*?><\/script>/mg)[0].substr(9).split("src=\"")[1].split("\"")[0];
    let script2 = page.response.match(/<\/script><script src=\"\/v2\/assets\/js\/main.*?(?=\")/mg)[0].substr(22);
    let originalPage = page.response.replace(/><\/script><script\ .*?vendors.*?><\/script>/mg,"></script>");
    originalPage = originalPage.replace(/<\/script><script src=\"\/v2\/assets\/js\/main.*?(?=\")/mg,"</script><script src=\"data:text/javascript,");
    let script = new XMLHttpRequest();
    script.open("GET","https://play.kahoot.it/"+scriptURL);
    script.send();
    script.onload = ()=>{
      const patchedScriptRegex = /\.onMessage=function\([a-z],[a-z]\)\{/mg;
      const letter1 = script.response.match(patchedScriptRegex)[0].match(/[a-z](?=,)/g)[0];
      const letter2 = script.response.match(patchedScriptRegex)[0].match(/[a-z](?=\))/g)[0];
      let patchedScript = script.response.replace(script.response.match(patchedScriptRegex)[0],`.onMessage=function(${letter1},${letter2}){window.globalMessageListener(${letter1},${letter2});`);
      let mainScript = new XMLHttpRequest();
      mainScript.open("GET","https://play.kahoot.it/"+script2);
      mainScript.send();
      mainScript.onload = ()=>{
        let sc = mainScript.response;
        let changed = originalPage.split("</body>");
        changed = `${changed[0]}<script>${patchedScript}</script><script>${sc}</script><script>window.globalMessageListener=${window.PinCheckerInjector.toString()};(${window.PinCheckerMain.toString()})();try{(${window.localStorage.kahootThemeScript})();}catch(err){}window.fireLoaded = window.parent.fireLoaded = true;</script></body>${changed[1]}`;
        console.log("[PIN-CHECKER] - 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();
        const doc = document.querySelector("iframe");
        doc.contentDocument.write(changed);
        document.title = doc.contentDocument.title;
      };
    };
  };
}else{
  console.warn("[PIN-CHECKER] - found AntiBot, waiting for injection");
  window.localStorage.extraCheck = window.PinCheckerMain.toString();
  window.localStorage.extraCheck2 = window.PinCheckerInjector.toString();
}