🏠 Home 

Greasy Fork is available in English.

KPin Checker

Check the pin of a kahoot game.

  1. // ==UserScript==
  2. // @name KPin Checker
  3. // @namespace http://tampermonkey.net/
  4. // @homepage https://theusaf.org
  5. // @version 2.0.0
  6. // @license MIT
  7. // @description Check the pin of a kahoot game.
  8. // @author theusaf
  9. // @match *://play.kahoot.it/*
  10. // @exclude *://play.kahoot.it/v2/assets/*
  11. // @copyright 2020-2023, Daniel Lau (https://github.com/theusaf/kahoot-antibot)
  12. // @grant none
  13. // @run-at document-start
  14. // ==/UserScript==
  15. /**
  16. * PinCheckerMain - The main pin checking function
  17. */
  18. function main() {
  19. function listenForTeamMode() {
  20. document
  21. .querySelector("[data-functional-selector=team-mode-card]")
  22. .addEventListener("click", () => {
  23. console.log("[PIN-CHECKER] - Entered team mode card.");
  24. setTimeout(() => {
  25. document
  26. .querySelector("[data-functional-selector=leave-game-mode-details]")
  27. .addEventListener("click", () => {
  28. console.log("[PIN-CHECKER] - Listening again");
  29. setTimeout(() => listenForTeamMode(), 250);
  30. });
  31. document
  32. .querySelector("[data-functional-selector=start-team-mode-button]")
  33. .addEventListener("click", () => {
  34. console.log("[PIN-CHECKER] - Using team mode.");
  35. window.localStorage.pinCheckerMode = "team";
  36. });
  37. }, 250);
  38. });
  39. }
  40. const loader = setInterval(() => {
  41. if (!document.querySelector("[data-functional-selector=team-mode-card]")) {
  42. return;
  43. }
  44. console.log("[PIN-CHECKER] - Ready!");
  45. clearInterval(loader);
  46. listenForTeamMode();
  47. if (window.localStorage.pinCheckerAutoRelogin === "true") {
  48. const waiter = setInterval(() => {
  49. let button = document.querySelector(
  50. "[data-functional-selector=classic-mode-card]"
  51. );
  52. if (window.localStorage.pinCheckerMode === "team") {
  53. button = document.querySelector(
  54. "[data-functional-selector=team-mode-card]"
  55. );
  56. }
  57. if (button && !button.disabled) {
  58. const guestButton = document.querySelector(
  59. "[data-functional-selector=play-as-guest-button]"
  60. );
  61. if (guestButton) {
  62. guestButton.click();
  63. }
  64. button.click();
  65. if (window.localStorage.pinCheckerMode === "team") {
  66. setTimeout(() => {
  67. document
  68. .querySelector(
  69. "[data-functional-selector=start-team-mode-button]"
  70. )
  71. .click();
  72. }, 250);
  73. }
  74. window.localStorage.pinCheckerAutoRelogin = false;
  75. if (
  76. +window.localStorage.pinCheckerLastQuizIndex <=
  77. window.kantibotData.kahootInternals.services.game.core.playList
  78. .length
  79. ) {
  80. kantibotData.kahootInternals.services.game.navigation.currentQuizIndex =
  81. +window.localStorage.pinCheckerLastQuizIndex ?? 0;
  82. }
  83. clearInterval(waiter);
  84. delete window.localStorage.pinCheckerMode;
  85. delete window.localStorage.pinCheckerLastQuizIndex;
  86. // check for start button
  87. }
  88. }, 500);
  89. } else {
  90. delete window.localStorage.pinCheckerMode;
  91. }
  92. }, 500);
  93. let loadChecks = 0;
  94. const themeLoadChecker = setInterval(() => {
  95. const errorButton = document.querySelector(
  96. '[data-functional-selector="dialog-actions"]'
  97. );
  98. if (errorButton) {
  99. clearInterval(themeLoadChecker);
  100. errorButton.querySelector("button").click();
  101. } else if (++loadChecks > 10) {
  102. clearInterval(themeLoadChecker);
  103. }
  104. }, 500);
  105. window.pinCheckerNameList = [];
  106. window.pinCheckerPin = null;
  107. window.pinCheckerSendIds = {};
  108. window.specialData = window.specialData || {};
  109. window.pinCheckerFalsePositive = false;
  110. window.pinCheckerFalsePositiveTimeout = null;
  111. /**
  112. * ResetGame - Reloads the page
  113. */
  114. function resetGame(message) {
  115. if (window.pinCheckerFalsePositive) {
  116. return console.log(
  117. "[PIN-CHECKER] - Detected false-positive broken pin. Not restarting."
  118. );
  119. }
  120. console.error(message || "[PIN-CHECKER] - Pin Broken. Attempting restart.");
  121. window.localStorage.pinCheckerAutoRelogin = true;
  122. window.localStorage.pinCheckerLastQuizIndex =
  123. window.kantibotData.kahootInternals.services.game.navigation.currentQuizIndex;
  124. window.document.write(
  125. "<scr" +
  126. "ipt>" +
  127. `window.location = "https://play.kahoot.it/v2/${window.location.search}";` +
  128. "</scr" +
  129. "ipt>"
  130. );
  131. }
  132. /**
  133. * concatTokens - From kahoot.js.org. Combines the tokens.
  134. *
  135. * @param {String} headerToken decoded token
  136. * @param {String} challengeToken decoded token 2
  137. * @returns {String} The final token
  138. */
  139. function concatTokens(headerToken, challengeToken) {
  140. // Combine the session token and the challenge token together to get the string needed to connect to the websocket endpoint
  141. let token = "";
  142. for (let i = 0; i < headerToken.length; i++) {
  143. const char = headerToken.charCodeAt(i),
  144. mod = challengeToken.charCodeAt(i % challengeToken.length),
  145. decodedChar = char ^ mod;
  146. token += String.fromCharCode(decodedChar);
  147. }
  148. return token;
  149. }
  150. /**
  151. * CreateClient - Creates a Kahoot! client to join a game
  152. * This really only works because kahoot treats kahoot.it, play.kahoot.it, etc as the same thing.
  153. *
  154. * @param {Number} pin The gameid
  155. */
  156. function createClient(pin) {
  157. console.log("[PIN-CHECKER] - Creating client");
  158. pin += "";
  159. const sessionRequest = new XMLHttpRequest();
  160. sessionRequest.open("GET", "/reserve/session/" + pin);
  161. sessionRequest.send();
  162. sessionRequest.onload = function () {
  163. let sessionData;
  164. try {
  165. sessionData = JSON.parse(sessionRequest.responseText);
  166. } catch (e) {
  167. // probably not found
  168. return resetGame();
  169. }
  170. const headerToken = atob(
  171. sessionRequest.getResponseHeader("x-kahoot-session-token")
  172. );
  173. let { challenge } = sessionData;
  174. challenge = challenge.replace(/(\u0009|\u2003)/gm, "");
  175. challenge = challenge.replace(/this /gm, "this");
  176. challenge = challenge.replace(/ *\. */gm, ".");
  177. challenge = challenge.replace(/ *\( */gm, "(");
  178. challenge = challenge.replace(/ *\) */gm, ")");
  179. challenge = challenge.replace("console.", "");
  180. challenge = challenge.replace("this.angular.isObject(offset)", "true");
  181. challenge = challenge.replace("this.angular.isString(offset)", "true");
  182. challenge = challenge.replace("this.angular.isDate(offset)", "true");
  183. challenge = challenge.replace("this.angular.isArray(offset)", "true");
  184. const merger =
  185. "var _ = {" +
  186. " replace: function() {" +
  187. " var args = arguments;" +
  188. " var str = arguments[0];" +
  189. " return str.replace(args[1], args[2]);" +
  190. " }" +
  191. "}; " +
  192. "var log = function(){};" +
  193. "return ",
  194. solver = Function(merger + challenge),
  195. headerChallenge = solver(),
  196. finalToken = concatTokens(headerToken, headerChallenge),
  197. connection = new WebSocket(
  198. `wss://kahoot.it/cometd/${pin}/${finalToken}`
  199. ),
  200. timesync = {};
  201. let shoken = false,
  202. clientId = "",
  203. messageId = 2,
  204. closed = false,
  205. name = "";
  206. connection.addEventListener("error", () => {
  207. console.error(
  208. "[PIN-CHECKER] - Socket connection failed. Assuming network connection is lost and realoading page."
  209. );
  210. resetGame();
  211. });
  212. connection.addEventListener("open", () => {
  213. connection.send(
  214. JSON.stringify([
  215. {
  216. advice: {
  217. interval: 0,
  218. timeout: 60000
  219. },
  220. minimumVersion: "1.0",
  221. version: "1.0",
  222. supportedConnectionTypes: ["websocket", "long-polling"],
  223. channel: "/meta/handshake",
  224. ext: {
  225. ack: true,
  226. timesync: {
  227. l: 0,
  228. o: 0,
  229. tc: Date.now()
  230. }
  231. },
  232. id: 1
  233. }
  234. ])
  235. );
  236. });
  237. connection.addEventListener("message", (m) => {
  238. const { data } = m,
  239. [message] = JSON.parse(data);
  240. if (message.channel === "/meta/handshake" && !shoken) {
  241. if (message.ext && message.ext.timesync) {
  242. shoken = true;
  243. clientId = message.clientId;
  244. const { tc, ts, p } = message.ext.timesync,
  245. l = Math.round((Date.now() - tc - p) / 2),
  246. o = ts - tc - l;
  247. Object.assign(timesync, {
  248. l,
  249. o,
  250. get tc() {
  251. return Date.now();
  252. }
  253. });
  254. connection.send(
  255. JSON.stringify([
  256. {
  257. advice: { timeout: 0 },
  258. channel: "/meta/connect",
  259. id: 2,
  260. ext: {
  261. ack: 0,
  262. timesync
  263. },
  264. clientId
  265. }
  266. ])
  267. );
  268. // start joining
  269. setTimeout(() => {
  270. name = "KCP_" + (Date.now() + "").substr(2);
  271. connection.send(
  272. JSON.stringify([
  273. {
  274. clientId,
  275. channel: "/service/controller",
  276. id: ++messageId,
  277. ext: {},
  278. data: {
  279. gameid: pin,
  280. host: "play.kahoot.it",
  281. content: JSON.stringify({
  282. device: {
  283. userAgent: window.navigator.userAgent,
  284. screen: {
  285. width: window.screen.width,
  286. height: window.screen.height
  287. }
  288. }
  289. }),
  290. name,
  291. type: "login"
  292. }
  293. }
  294. ])
  295. );
  296. }, 1000);
  297. }
  298. } else if (message.channel === "/meta/connect" && shoken && !closed) {
  299. connection.send(
  300. JSON.stringify([
  301. {
  302. channel: "/meta/connect",
  303. id: ++messageId,
  304. ext: {
  305. ack: message.ext.ack,
  306. timesync
  307. },
  308. clientId
  309. }
  310. ])
  311. );
  312. } else if (message.channel === "/service/controller") {
  313. if (message.data && message.data.type === "loginResponse") {
  314. if (message.data.error === "NONEXISTING_SESSION") {
  315. // session doesn't exist
  316. connection.send(
  317. JSON.stringify([
  318. {
  319. channel: "/meta/disconnect",
  320. clientId,
  321. id: ++messageId,
  322. ext: {
  323. timesync
  324. }
  325. }
  326. ])
  327. );
  328. connection.close();
  329. resetGame();
  330. } else {
  331. // Check if the client is in the game after 10 seconds
  332. setTimeout(() => {
  333. if (!window.pinCheckerNameList.includes(name)) {
  334. // Uh oh! the client didn't join!
  335. resetGame();
  336. }
  337. }, 10e3);
  338. console.log(
  339. "[PIN-CHECKER] - Client joined game. Connection is good."
  340. );
  341. // good. leave the game.
  342. connection.send(
  343. JSON.stringify([
  344. {
  345. channel: "/meta/disconnect",
  346. clientId,
  347. id: ++messageId,
  348. ext: {
  349. timesync
  350. }
  351. }
  352. ])
  353. );
  354. closed = true;
  355. setTimeout(() => {
  356. connection.close();
  357. }, 500);
  358. }
  359. }
  360. } else if (message.channel === "/service/status") {
  361. if (message.data.status === "LOCKED") {
  362. // locked, cannot test
  363. console.log("[PIN-CHECKER] - Game is locked. Unable to test.");
  364. closed = true;
  365. connection.send(
  366. JSON.stringify([
  367. {
  368. channel: "/meta/disconnect",
  369. clientId,
  370. id: ++messageId,
  371. ext: {
  372. timesync
  373. }
  374. }
  375. ])
  376. );
  377. setTimeout(() => {
  378. connection.close();
  379. }, 500);
  380. }
  381. }
  382. });
  383. };
  384. }
  385. window.pinCheckerInterval = setInterval(() => {
  386. if (window.pinCheckerPin) {
  387. createClient(window.pinCheckerPin);
  388. }
  389. }, 60 * 1000);
  390. /**
  391. * pinCheckerSendInjector
  392. * - Checks the sent messages to ensure events are occuring
  393. * - This is a small fix for a bug in Kahoot.
  394. *
  395. * @param {String} data The sent message.
  396. */
  397. window.pinCheckerSendInjector = function pinCheckerSendInjector(data) {
  398. data = JSON.parse(data)[0];
  399. const now = Date.now();
  400. let content = {};
  401. try {
  402. content = JSON.parse(data.data.content);
  403. } catch (e) {
  404. /* likely no content */
  405. }
  406. if (data.data && typeof data.data.id !== "undefined") {
  407. for (const i in window.pinCheckerSendIds) {
  408. window.pinCheckerSendIds[i].add(data.data.id);
  409. }
  410. // content slides act differently, ignore them
  411. if (content.gameBlockType === "content") return;
  412. /**
  413. * Checks for events and attempts to make sure that it succeeds (doesn't crash)
  414. * - deprecated, kept in just in case for the moment
  415. *
  416. * @param {Number} data.data.id The id of the action
  417. */
  418. switch (data.data.id) {
  419. case 9: {
  420. window.pinCheckerSendIds[now] = new Set();
  421. setTimeout(() => {
  422. if (!window.pinCheckerSendIds[now].has(1)) {
  423. // Restart, likely stuck
  424. resetGame(
  425. "[PIN-CHECKER] - Detected stuck on loading screen. Reloading the page."
  426. );
  427. } else {
  428. delete window.pinCheckerSendIds[now];
  429. }
  430. }, 60e3);
  431. break;
  432. }
  433. case 1: {
  434. window.pinCheckerSendIds[now] = new Set();
  435. setTimeout(() => {
  436. if (!window.pinCheckerSendIds[now].has(2)) {
  437. // Restart, likely stuck
  438. resetGame(
  439. "[PIN-CHECKER] - Detected stuck on get ready screen. Reloading the page."
  440. );
  441. } else {
  442. delete window.pinCheckerSendIds[now];
  443. }
  444. }, 60e3);
  445. break;
  446. }
  447. case 2: {
  448. window.pinCheckerSendIds[now] = new Set();
  449. // wait up to 5 minutes, assume something wrong
  450. setTimeout(() => {
  451. if (
  452. !window.pinCheckerSendIds[now].has(4) &&
  453. !window.pinCheckerSendIds[now].has(8)
  454. ) {
  455. // Restart, likely stuck
  456. resetGame(
  457. "[PIN-CHECKER] - Detected stuck on question answer. Reloading the page."
  458. );
  459. } else {
  460. delete window.pinCheckerSendIds[now];
  461. }
  462. }, 300e3);
  463. break;
  464. }
  465. }
  466. }
  467. };
  468. /**
  469. * closeError
  470. * - Used when the game is closed and fails to reconnect properly
  471. */
  472. window.closeError = function () {
  473. resetGame("[PIN-CHECKER] - Detected broken disconnected game, reloading!");
  474. };
  475. }
  476. /**
  477. * PinCheckerInjector - Checks messages and stores the names of players who joined within the last few seconds
  478. *
  479. * @param {String} message The websocket message
  480. */
  481. function messageInjector(socket, message) {
  482. function pinCheckerFalsePositiveReset() {
  483. window.pinCheckerFalsePositive = true;
  484. clearTimeout(window.pinCheckerFalsePositiveTimeout);
  485. window.pinCheckerFalsePositiveTimeout = setTimeout(function () {
  486. window.pinCheckerFalsePositive = false;
  487. }, 15e3);
  488. }
  489. const data = JSON.parse(message.data)[0];
  490. if (!socket.webSocket.pinCheckClose) {
  491. socket.webSocket.pinCheckClose = socket.webSocket.onclose;
  492. socket.webSocket.onclose = function () {
  493. socket.webSocket.pinCheckClose();
  494. setTimeout(() => {
  495. const stillNotConnected = document.querySelector(
  496. '[data-functional-selector="disconnected-page"]'
  497. );
  498. if (stillNotConnected) {
  499. window.closeError();
  500. }
  501. }, 30e3);
  502. };
  503. }
  504. if (!socket.webSocket.pinCheckSend) {
  505. socket.webSocket.pinCheckSend = socket.webSocket.send;
  506. socket.webSocket.send = function (data) {
  507. window.pinCheckerSendInjector(data);
  508. socket.webSocket.pinCheckSend(data);
  509. };
  510. }
  511. try {
  512. const part =
  513. document.querySelector('[data-functional-selector="game-pin"]') ||
  514. document.querySelector(
  515. '[data-functional-selector="bottom-bar-game-pin"]'
  516. );
  517. if (
  518. Number(part.innerText) != window.pinCheckerPin &&
  519. Number(part.innerText) != 0 &&
  520. !isNaN(Number(part.innerText))
  521. ) {
  522. window.pinCheckerPin = Number(part.innerText);
  523. console.log(
  524. "[PIN-CHECKER] - Discovered new PIN: " + window.pinCheckerPin
  525. );
  526. } else if (Number(part.innerText) == 0 || isNaN(Number(part.innerText))) {
  527. window.pinCheckerPin = null;
  528. console.log(
  529. "[PIN-CHECKER] - PIN is hidden or game is locked. Unable to test."
  530. );
  531. }
  532. } catch (err) {
  533. /* Unable to get pin, hidden */
  534. }
  535. if (data.data && data.data.type === "joined") {
  536. pinCheckerFalsePositiveReset();
  537. window.pinCheckerNameList.push(data.data.name);
  538. setTimeout(() => {
  539. // remove after 20 seconds (for performance)
  540. window.pinCheckerNameList.splice(0, 1);
  541. }, 20e3);
  542. } else if (data.data && data.data.id === 45) {
  543. pinCheckerFalsePositiveReset();
  544. }
  545. }
  546. window.kantibotAddHook({
  547. prop: "onMessage",
  548. condition: (target, value) =>
  549. typeof value === "function" &&
  550. typeof target.reset === "function" &&
  551. typeof target.onOpen === "function",
  552. callback: (target, value) => {
  553. console.log(target, value);
  554. target.onMessage = new Proxy(target.onMessage, {
  555. apply: function (target, thisArg, argumentsList) {
  556. messageInjector(argumentsList[0], argumentsList[1]);
  557. return target.apply(thisArg, argumentsList);
  558. }
  559. });
  560. return true;
  561. }
  562. });
  563. main();