// ==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();