Greasy Fork is available in English.
Check the pin of a kahoot game.
- // ==UserScript==
- // @name KPin Checker
- // @namespace http://tampermonkey.net/
- // @homepage https://theusaf.org
- // @version 2.0.0
- // @license MIT
- // @description Check the pin of a kahoot game.
- // @author theusaf
- // @match *://play.kahoot.it/*
- // @exclude *://play.kahoot.it/v2/assets/*
- // @copyright 2020-2023, Daniel Lau (https://github.com/theusaf/kahoot-antibot)
- // @grant none
- // @run-at document-start
- // ==/UserScript==
- /**
- * PinCheckerMain - The main pin checking function
- */
- function main() {
- 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=leave-game-mode-details]")
- .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.");
- window.localStorage.pinCheckerMode = "team";
- });
- }, 250);
- });
- }
- const loader = setInterval(() => {
- if (!document.querySelector("[data-functional-selector=team-mode-card]")) {
- return;
- }
- console.log("[PIN-CHECKER] - Ready!");
- clearInterval(loader);
- listenForTeamMode();
- if (window.localStorage.pinCheckerAutoRelogin === "true") {
- const waiter = setInterval(() => {
- let button = document.querySelector(
- "[data-functional-selector=classic-mode-card]"
- );
- if (window.localStorage.pinCheckerMode === "team") {
- button = document.querySelector(
- "[data-functional-selector=team-mode-card]"
- );
- }
- if (button && !button.disabled) {
- const guestButton = document.querySelector(
- "[data-functional-selector=play-as-guest-button]"
- );
- if (guestButton) {
- guestButton.click();
- }
- button.click();
- if (window.localStorage.pinCheckerMode === "team") {
- setTimeout(() => {
- document
- .querySelector(
- "[data-functional-selector=start-team-mode-button]"
- )
- .click();
- }, 250);
- }
- window.localStorage.pinCheckerAutoRelogin = false;
- if (
- +window.localStorage.pinCheckerLastQuizIndex <=
- window.kantibotData.kahootInternals.services.game.core.playList
- .length
- ) {
- kantibotData.kahootInternals.services.game.navigation.currentQuizIndex =
- +window.localStorage.pinCheckerLastQuizIndex ?? 0;
- }
- clearInterval(waiter);
- delete window.localStorage.pinCheckerMode;
- delete window.localStorage.pinCheckerLastQuizIndex;
- // check for start button
- }
- }, 500);
- } else {
- delete window.localStorage.pinCheckerMode;
- }
- }, 500);
- let loadChecks = 0;
- const themeLoadChecker = setInterval(() => {
- const errorButton = document.querySelector(
- '[data-functional-selector="dialog-actions"]'
- );
- if (errorButton) {
- clearInterval(themeLoadChecker);
- errorButton.querySelector("button").click();
- } else if (++loadChecks > 10) {
- clearInterval(themeLoadChecker);
- }
- }, 500);
- window.pinCheckerNameList = [];
- window.pinCheckerPin = null;
- window.pinCheckerSendIds = {};
- window.specialData = window.specialData || {};
- window.pinCheckerFalsePositive = false;
- window.pinCheckerFalsePositiveTimeout = null;
- /**
- * ResetGame - Reloads the page
- */
- function resetGame(message) {
- if (window.pinCheckerFalsePositive) {
- return console.log(
- "[PIN-CHECKER] - Detected false-positive broken pin. Not restarting."
- );
- }
- console.error(message || "[PIN-CHECKER] - Pin Broken. Attempting restart.");
- window.localStorage.pinCheckerAutoRelogin = true;
- window.localStorage.pinCheckerLastQuizIndex =
- window.kantibotData.kahootInternals.services.game.navigation.currentQuizIndex;
- window.document.write(
- "<scr" +
- "ipt>" +
- `window.location = "https://play.kahoot.it/v2/${window.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 sessionRequest = new XMLHttpRequest();
- sessionRequest.open("GET", "/reserve/session/" + pin);
- sessionRequest.send();
- sessionRequest.onload = function () {
- let sessionData;
- try {
- sessionData = JSON.parse(sessionRequest.responseText);
- } catch (e) {
- // probably not found
- return resetGame();
- }
- const headerToken = atob(
- sessionRequest.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),
- headerChallenge = solver(),
- finalToken = concatTokens(headerToken, headerChallenge),
- connection = new WebSocket(
- `wss://kahoot.it/cometd/${pin}/${finalToken}`
- ),
- timesync = {};
- let shoken = false,
- clientId = "",
- messageId = 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: ++messageId,
- ext: {},
- data: {
- gameid: pin,
- host: "play.kahoot.it",
- content: JSON.stringify({
- device: {
- userAgent: window.navigator.userAgent,
- screen: {
- width: window.screen.width,
- height: window.screen.height
- }
- }
- }),
- name,
- type: "login"
- }
- }
- ])
- );
- }, 1000);
- }
- } else if (message.channel === "/meta/connect" && shoken && !closed) {
- connection.send(
- JSON.stringify([
- {
- channel: "/meta/connect",
- id: ++messageId,
- 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: ++messageId,
- ext: {
- timesync
- }
- }
- ])
- );
- connection.close();
- resetGame();
- } else {
- // Check if the client is in the game after 10 seconds
- setTimeout(() => {
- if (!window.pinCheckerNameList.includes(name)) {
- // Uh oh! the client didn't join!
- resetGame();
- }
- }, 10e3);
- console.log(
- "[PIN-CHECKER] - Client joined game. Connection is good."
- );
- // good. leave the game.
- connection.send(
- JSON.stringify([
- {
- channel: "/meta/disconnect",
- clientId,
- id: ++messageId,
- 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: ++messageId,
- ext: {
- timesync
- }
- }
- ])
- );
- setTimeout(() => {
- connection.close();
- }, 500);
- }
- }
- });
- };
- }
- window.pinCheckerInterval = setInterval(() => {
- if (window.pinCheckerPin) {
- createClient(window.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.
- */
- window.pinCheckerSendInjector = function pinCheckerSendInjector(data) {
- data = JSON.parse(data)[0];
- const now = 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 window.pinCheckerSendIds) {
- window.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: {
- window.pinCheckerSendIds[now] = new Set();
- setTimeout(() => {
- if (!window.pinCheckerSendIds[now].has(1)) {
- // Restart, likely stuck
- resetGame(
- "[PIN-CHECKER] - Detected stuck on loading screen. Reloading the page."
- );
- } else {
- delete window.pinCheckerSendIds[now];
- }
- }, 60e3);
- break;
- }
- case 1: {
- window.pinCheckerSendIds[now] = new Set();
- setTimeout(() => {
- if (!window.pinCheckerSendIds[now].has(2)) {
- // Restart, likely stuck
- resetGame(
- "[PIN-CHECKER] - Detected stuck on get ready screen. Reloading the page."
- );
- } else {
- delete window.pinCheckerSendIds[now];
- }
- }, 60e3);
- break;
- }
- case 2: {
- window.pinCheckerSendIds[now] = new Set();
- // wait up to 5 minutes, assume something wrong
- setTimeout(() => {
- if (
- !window.pinCheckerSendIds[now].has(4) &&
- !window.pinCheckerSendIds[now].has(8)
- ) {
- // Restart, likely stuck
- resetGame(
- "[PIN-CHECKER] - Detected stuck on question answer. Reloading the page."
- );
- } else {
- delete window.pinCheckerSendIds[now];
- }
- }, 300e3);
- break;
- }
- }
- }
- };
- /**
- * closeError
- * - Used when the game is closed and fails to reconnect properly
- */
- window.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
- */
- function messageInjector(socket, message) {
- function pinCheckerFalsePositiveReset() {
- window.pinCheckerFalsePositive = true;
- clearTimeout(window.pinCheckerFalsePositiveTimeout);
- window.pinCheckerFalsePositiveTimeout = setTimeout(function () {
- window.pinCheckerFalsePositive = false;
- }, 15e3);
- }
- const 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) {
- window.closeError();
- }
- }, 30e3);
- };
- }
- if (!socket.webSocket.pinCheckSend) {
- socket.webSocket.pinCheckSend = socket.webSocket.send;
- socket.webSocket.send = function (data) {
- window.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) != window.pinCheckerPin &&
- Number(part.innerText) != 0 &&
- !isNaN(Number(part.innerText))
- ) {
- window.pinCheckerPin = Number(part.innerText);
- console.log(
- "[PIN-CHECKER] - Discovered new PIN: " + window.pinCheckerPin
- );
- } else if (Number(part.innerText) == 0 || isNaN(Number(part.innerText))) {
- window.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();
- window.pinCheckerNameList.push(data.data.name);
- setTimeout(() => {
- // remove after 20 seconds (for performance)
- window.pinCheckerNameList.splice(0, 1);
- }, 20e3);
- } else if (data.data && data.data.id === 45) {
- pinCheckerFalsePositiveReset();
- }
- }
- window.kantibotAddHook({
- prop: "onMessage",
- condition: (target, value) =>
- typeof value === "function" &&
- typeof target.reset === "function" &&
- typeof target.onOpen === "function",
- callback: (target, value) => {
- console.log(target, value);
- target.onMessage = new Proxy(target.onMessage, {
- apply: function (target, thisArg, argumentsList) {
- messageInjector(argumentsList[0], argumentsList[1]);
- return target.apply(thisArg, argumentsList);
- }
- });
- return true;
- }
- });
- main();