// ==UserScript==
// @name Kahoot PIN Checker
// @namespace http://tampermonkey.net/
// @version 1.1.3
// @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,
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;
windw.PinCheckerSendIDs = {};
/**
* ResetGame - Reloads the page
*/
function ResetGame(message){
console.error(message || "[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
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)/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 ",
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;
}
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;
}
}
}
};
};
/**
* 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,
data = JSON.parse(message.data)[0];
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"){
windw.PinCheckerNameList.push(data.data.name);
setTimeout(()=>{
// remove after 20 seconds (for performance)
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.");
const page = new XMLHttpRequest();
page.open("GET",location.href);
page.send();
page.onload = function(){
const scriptURL = page.response.match(/><\/script><script .*?vendors.*?><\/script>/mg)[0].substr(9).split("src=\"")[1].split("\"")[0],
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,");
const script = new XMLHttpRequest();
script.open("GET","https://play.kahoot.it/"+scriptURL);
script.send();
script.onload = ()=>{
const patchedScriptRegex = /\.onMessage=function\([a-z],[a-z]\)\{/mg,
letter1 = script.response.match(patchedScriptRegex)[0].match(/[a-z](?=,)/g)[0],
letter2 = script.response.match(patchedScriptRegex)[0].match(/[a-z](?=\))/g)[0],
patchedScript = script.response.replace(script.response.match(patchedScriptRegex)[0],`.onMessage=function(${letter1},${letter2}){window.globalMessageListener(${letter1},${letter2});`),
mainScript = new XMLHttpRequest();
mainScript.open("GET","https://play.kahoot.it/"+script2);
mainScript.send();
mainScript.onload = ()=>{
const 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();
}