您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Check the pin of a kahoot game.
当前为
// ==UserScript== // @name KPin Checker // @namespace http://tampermonkey.net/ // @homepage https://theusaf.org // @version 1.3.7 // @license MIT // @description Check the pin of a kahoot game. // @author theusaf // @match *://play.kahoot.it/* // @exclude *://play.kahoot.it/v2/assets/* // @icon https://kahoot-win.com/resource/img/game/medal/gold.svg // @copyright 2020-2021, Daniel Lau (https://github.com/theusaf/kahoot-antibot) // @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 () { function listenForTeamMode() { document .querySelector("[data-functional-selector=team-mode-card]") .addEventListener("click", () => { console.log("[PIN-CHECKER] - Entered team mode card."); setTimeout(() => { document .querySelector( "[data-functional-selector=launch-page] > div:nth-of-type(4) button" ) .addEventListener("click", () => { console.log("[PIN-CHECKER] - Listening again"); setTimeout(() => listenForTeamMode(), 250); }); document .querySelector("[data-functional-selector=start-team-mode-button]") .addEventListener("click", () => { console.log("[PIN-CHECKER] - Using team mode."); windw.localStorage.PinCheckerMode = "team"; }); }, 250); }); } const windw = window.parent; window.windw = windw; const loader = setInterval(() => { if (!document.querySelector("[data-functional-selector=team-mode-card]")) { return; } console.log("[PIN-CHECKER] - Ready!"); clearInterval(loader); listenForTeamMode(); if (document.querySelector("#antibotwtr")) { const p = document.createElement("p"); p.innerHTML = "[KPC] v1.3.7"; document.querySelector("#antibotwtr").append(p); } if (windw.localStorage.PinCheckerAutoRelogin === "true") { const waiter = setInterval(() => { let a = document.querySelector( "[data-functional-selector=classic-mode-card]" ); if (windw.localStorage.PinCheckerMode === "team") { a = document.querySelector( "[data-functional-selector=team-mode-card]" ); } if (a && !a.disabled) { const guestButton = document.querySelector( "[data-functional-selector=play-as-guest-button]" ); if (guestButton) { guestButton.click(); } a.click(); if (windw.localStorage.PinCheckerMode === "team") { setTimeout(() => { document .querySelector( "[data-functional-selector=start-team-mode-button]" ) .click(); }, 250); } windw.localStorage.PinCheckerAutoRelogin = false; if ( +windw.localStorage.PinCheckerLastQuizIndex <= ( windw.specialData.kahootCore || windw.antibotData.kahootInternals.kahootCore ).game.core.playList.length ) { ( windw.specialData.kahootCore || windw.antibotData.kahootInternals.kahootCore ).game.navigation.currentQuizIndex = +windw.localStorage.PinCheckerLastQuizIndex || 0; } clearInterval(waiter); delete windw.localStorage.PinCheckerMode; delete windw.localStorage.PinCheckerLastQuizIndex; // check for start button } }, 500); } else { delete windw.localStorage.PinCheckerMode; } }, 500); windw.PinCheckerNameList = []; windw.PinCheckerPin = null; windw.PinCheckerSendIDs = {}; windw.specialData = windw.specialData || {}; windw.PinCheckerFalsePositive = false; windw.PinCheckerFalsePositiveTimeout = null; /** * ResetGame - Reloads the page */ function ResetGame(message) { if (windw.PinCheckerFalsePositive) { return console.log( "[PIN-CHECKER] - Detected false-positive broken pin. Not restarting." ); } console.error(message || "[PIN-CHECKER] - Pin Broken. Attempting restart."); windw.localStorage.PinCheckerAutoRelogin = true; windw.localStorage.PinCheckerLastQuizIndex = ( windw.specialData.kahootCore || windw.antibotData.kahootInternals.kahootCore ).game.navigation.currentQuizIndex; 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 let token = ""; for (let i = 0; i < headerToken.length; i++) { const char = headerToken.charCodeAt(i), mod = challengeToken.charCodeAt(i % challengeToken.length), 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 += ""; 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)/gm, ""); challenge = challenge.replace(/this /gm, "this"); challenge = challenge.replace(/ *\. */gm, "."); challenge = challenge.replace(/ *\( */gm, "("); challenge = challenge.replace(/ *\) */gm, ")"); 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 ", solver = Function(merger + challenge), ChallengeHeader = solver(), FinalToken = concatTokens(TokenHeader, ChallengeHeader), connection = new WebSocket( "wss://play.kahoot.it/cometd/" + pin + "/" + FinalToken ), timesync = {}; let shoken = false, clientId = "", mid = 2, closed = false, name = ""; connection.addEventListener("error", () => { console.error( "[PIN-CHECKER] - Socket connection failed. Assuming network connection is lost and realoading page." ); ResetGame(); }); 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, }, ]) ); }); connection.addEventListener("message", (m) => { const { data } = m, [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); /** * PinCheckerSendInjector * - Checks the sent messages to ensure events are occuring * - This is a small fix for a bug in Kahoot. * * @param {String} data The sent message. */ windw.PinCheckerSendInjector = function (data) { data = JSON.parse(data)[0]; const n = Date.now(); let content = {}; try { content = JSON.parse(data.data.content); } catch (e) { /* likely no content */ } if (data.data && typeof data.data.id !== "undefined") { for (const i in windw.PinCheckerSendIDs) { windw.PinCheckerSendIDs[i].add(data.data.id); } // content slides act differently, ignore them if (content.gameBlockType === "content") { return; } /** * Checks for events and attempts to make sure that it succeeds (doesn't crash) * - deprecated, kept in just in case for the moment * * @param {Number} data.data.id The id of the action */ switch (data.data.id) { case 9: { windw.PinCheckerSendIDs[n] = new Set(); setTimeout(() => { if (!windw.PinCheckerSendIDs[n].has(1)) { // Restart, likely stuck ResetGame( "[PIN-CHECKER] - Detected stuck on loading screen. Reloading the page." ); } else { delete windw.PinCheckerSendIDs[n]; } }, 60e3); break; } case 1: { windw.PinCheckerSendIDs[n] = new Set(); setTimeout(() => { if (!windw.PinCheckerSendIDs[n].has(2)) { // Restart, likely stuck ResetGame( "[PIN-CHECKER] - Detected stuck on get ready screen. Reloading the page." ); } else { delete windw.PinCheckerSendIDs[n]; } }, 60e3); break; } case 2: { windw.PinCheckerSendIDs[n] = new Set(); // wait up to 5 minutes, assume something wrong setTimeout(() => { if ( !windw.PinCheckerSendIDs[n].has(4) && !windw.PinCheckerSendIDs[n].has(8) ) { // Restart, likely stuck ResetGame( "[PIN-CHECKER] - Detected stuck on question answer. Reloading the page." ); } else { delete windw.PinCheckerSendIDs[n]; } }, 300e3); break; } } } }; /** * CloseError * - Used when the game is closed and fails to reconnect properly */ windw.CloseError = function () { ResetGame("[PIN-CHECKER] - Detected broken disconnected game, reloading!"); }; }; /** * 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) { function PinCheckerFalsePositiveReset() { windw.PinCheckerFalsePositive = true; clearTimeout(windw.PinCheckerFalsePositiveTimeout); windw.PinCheckerFalsePositiveTimeout = setTimeout(function () { windw.PinCheckerFalsePositive = false; }, 15e3); } const windw = window.parent, data = JSON.parse(message.data)[0]; if (!socket.webSocket.PinCheckClose) { socket.webSocket.PinCheckClose = socket.webSocket.onclose; socket.webSocket.onclose = function () { socket.webSocket.PinCheckClose(); setTimeout(() => { const StillNotConnected = document.querySelector( "[data-functional-selector=\"disconnected-page\"]" ); if (StillNotConnected) { windw.CloseError(); } }, 30e3); }; } if (!socket.webSocket.PinCheckSend) { if (windw.page) { // Antibot exists, don't overwrite. if (socket.webSocket.oldSend) { socket.webSocket.PinCheckSend = socket.webSocket.oldSend; socket.webSocket.AntiBotSendData = socket.webSocket.send; socket.webSocket.send = function (data) { windw.PinCheckerSendInjector(data); socket.webSocket.AntiBotSendData(data); }; } return; } socket.webSocket.PinCheckSend = socket.webSocket.send; socket.webSocket.send = function (data) { windw.PinCheckerSendInjector(data); socket.webSocket.PinCheckSend(data); }; } 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) { /* Unable to get pin, hidden */ } if (data.data && data.data.type === "joined") { PinCheckerFalsePositiveReset(); windw.PinCheckerNameList.push(data.data.name); setTimeout(() => { // remove after 20 seconds (for performance) windw.PinCheckerNameList.splice(0, 1); }, 20e3); } else if (data.data && data.data.id === 45) { PinCheckerFalsePositiveReset(); } }; function createBlobURL(script) { return URL.createObjectURL( new Blob([script], { type: "application/javascript" }) ); } if (!window.kantibotEnabled && !window.page) { document.write( "<p id=\"pin-checker-loading-notice\">[PIN-CHECKER] - Patching Kahoot. Please wait.</p><p>If this screen stays blank for a long time, report an issue in <a href=\"https://discord.gg/pPdvXU6\">Discord</a>, <a href=\"https://github.com/theusaf/kantibot\">GitHub</a>, or <a href=\"https://greasyfork.org/en/scripts/392154-kpin-checker\">Greasyfork</a>.</p>" ); const page = new XMLHttpRequest(); page.open("GET", location.href); page.send(); page.onload = function () { const [scriptURL] = page.response.match( /\/\/assets-cdn.*\/v2\/assets\/vendor.*?(?=")/ ), [script2] = page.response.match( /\/\/assets-cdn.*\/v2\/assets\/index.*?(?=")/ ), originalPage = page.response .replace(/<script type="module".*?<\/script>/, "") .replace(/<link type="modulepreload".*?>/gm, ""), script = new XMLHttpRequest(); script.open("GET", scriptURL); script.send(); script.onload = () => { const patchedScriptRegex = /\.onMessage=function\([$\w]+,[$\w]+\)\{/, [letter1] = script.response .match(patchedScriptRegex)[0] .match(/[$\w]+(?=,)/), [letter2] = script.response .match(patchedScriptRegex)[0] .match(/[$\w]+(?=\))/), patchedScript = script.response.replace( script.response.match(patchedScriptRegex)[0], `.onMessage=function(${letter1},${letter2}){window.globalMessageListener(${letter1},${letter2});` ), mainScript = new XMLHttpRequest(); mainScript.open("GET", script2); mainScript.send(); mainScript.onload = () => { let { response: sc } = mainScript; // Access the core data const cr = /[$\w]+\.game\.core/, letter6 = sc.match(cr)[0].match(/[$\w]+(?=\.game)/)[0]; sc = sc.replace( cr, `(()=>{ if (typeof windw !== "undefined") { windw.specialData.kahootCore = ${letter6}; } return ${letter6}.game.core; })()` ); // fix imports const vendorsBlobURL = createBlobURL(patchedScript); sc = sc.replace( /from".\/vendor.*?";/, `from"${vendorsBlobURL}";URL.revokeObjectURL("${vendorsBlobURL}");` ); sc = sc.replace( /import\("\./gm, "import(\"https://assets-cdn.kahoot.it/player/v2/assets/" ); const mainBlobURL = createBlobURL(sc); let changed = originalPage.split("</body>"); changed = `${changed[0]} <script> import("${mainBlobURL}").then(() => { URL.revokeObjectURL("${mainBlobURL}"); }); </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; doc.addEventListener("load", () => { window.location.reload(); }); }; }; }; } else { console.warn("[PIN-CHECKER] - found AntiBot, waiting for injection"); window.localStorage.extraCheck = window.PinCheckerMain.toString(); window.localStorage.extraCheck2 = window.PinCheckerInjector.toString(); }