UNPKG

kahoot.js-latest

Version:
734 lines (700 loc) 23.5 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: util/ChallengeHandler.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: util/ChallengeHandler.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/** * @fileinfo This is the ChallengeHandler module * - Loads stuff used in challenges. */ const EventEmitter = require("events"); const {URL} = require("url"); const http = require("http"); const https = require("https"); const emoji = require("./emoji.js"); // overwrites functions function Injector(){ this.data = this.data || {}; Object.assign(this.data,{ totalScore: 0, totalScoreNoBonus: 0, totalStreak: 0, streak: -1, phase: "start", questionIndex: 0, correctCount: 0, incorrectCount: 0, unansweredCount: 0 }); this.answer = async (choice,empty)=>{ clearTimeout(this.ti); const question = this.challengeData.kahoot.questions[this.data.questionIndex]; let tick = Date.now() - this.receivedQuestionTime; if(this.defaults.options.ChallengeGetFullScore || this.defaults.options.ChallengeWaitForInput || !this.challengeData.challenge.game_options.question_timer){ tick = 1; } const pointsQuestion = this.challengeData.progress.questions.reverse()[0].pointsQuestion || false; if(+this.defaults.options.ChallengeScore > 1500){ // Hard limit of 0 - 1,500. (anything higher = 0 on Kahoot's end) this.defaults.options.ChallengeScore = 1500; }else if(+this.defaults.options.ChallengeScore &lt; 0){ this.defaults.options.ChallengeScore = 0; } const timeScore = +this.defaults.options.ChallengeScore || ((Math.round((1 - ((tick / question.time) / 2)) * 1000) * question.pointsMultiplier) * +pointsQuestion); if(this.data.streak === -1){ this.data.streak = 0; const ent = this.challengeData.progress.playerProgress.playerProgressEntries; let falseScore = 0; for(let q in ent){ if(q >= this.data.questionIndex){ break; } if(!ent[q].questionMetrics){ break; } if(ent[q].questionMetrics[this.name] > falseScore || !this.challengeData.kahoot.questions[q].points){ this.data.streak++; }else{ this.data.streak = 0; } falseScore = ent[q].questionMetrics[this.name]; this.data.score = falseScore; } } const alwaysCorrect = this.defaults.options.ChallengeAlwaysCorrect; let correct = false; let text = ""; if(empty === null){ choice = -1; } let choiceIndex = +choice; let c2 = []; let score = 0; switch (question.type) { case "quiz":{ try{ correct = question.choices[choiceIndex].correct; text = question.choices[choiceIndex].answer; }catch(e){ correct = false; text = ""; }finally{ if(alwaysCorrect){correct = true;} if(correct){score+=timeScore;} } break; } case "jumble":{ correct = JSON.stringify(choice) === JSON.stringify([0,1,2,3]); if(typeof choice != "object" || typeof choice.length === "undefined"){ choice = []; } for(let j in choice){ choice[j] = +choice[j]; } let tmpList = []; for(let n of choice){ try{ tmpList.push(question.choices[n].answer); }catch(e){ tmpList.push(""); } } text = tmpList.join("|"); choiceIndex = -1; if(alwaysCorrect){correct = true;} if(correct){score+=timeScore;} break; } case "multiple_select_quiz":{ if(typeof choice != "object" || typeof choice.length === "undefined"){ choice = []; } correct = true; for(let i in choice){ choice[i] = +choice[i]; } for(let ch in question.choices){ if(question.choices[ch].correct){ c2.push(ch); if(choice.includes(+ch) || alwaysCorrect){ if(correct){ score += timeScore; } }else{ score = 0; correct = false; } } } break; } case "open_ended":{ text = choice + ""; const invalid = /[~`!@#$%^&amp;*(){}[\];:"'&lt;,.>?/\\|\-_+=]/gm; const test = text.replace(invalid,""); for(let choice of question.choices){ if(choice.answer.replace(emoji,"")){ correct = test.replace(emoji,"").toLowerCase() == choice.answer.replace(emoji,"").replace(invalid,"").toLowerCase(); }else{ correct = test == choice; } if(correct){ choiceIndex = question.choices.indexOf(choice); break; } } if(alwaysCorrect){correct = true;} if(correct){score+=timeScore;} break; } case "word_cloud":{ text = choice + ""; choiceIndex = -1; correct = true; if(this.defaults.options.ChallengeScore){ score += timeScore; } break; } default:{ choiceIndex = +choice || 0; correct = true; if(this.defaults.options.ChallengeScore){ score += timeScore; } } } let c = []; if(question.choices){ for(let choice of question.choices){ if(choice.correct){ c.push(choice.answer); } } } const oldstreak = this.data.streak; if(correct){this.data.streak++;}else{this.data.streak = 0;} const payload = { device: { screen: { width: 1920, height: 1080 }, userAgent: this.userAgent }, gameMode: this.challengeData.progress.gameMode, gameOptions: this.challengeData.progress.gameOptions, hostOriganizationId: null, kickedPlayers: [], numQuestions: this.challengeData.kahoot.questions.length, organizationId: "", question: { answers: [ { bonusPoints: { answerStreakBonus: this._calculateStreakBonus() }, choiceIndex, isCorrect: correct, playerCid: +this.cid, playerId: this.name, points: +correct * score, reactionTime: tick, receivedTime: Date.now(), text } ], choices: question.choices, duration: question.time, format: question.questionFormat, index: this.data.questionIndex, lag: 0, layout: question.layout, playerCount: 1, pointsQuestion, skipped: (empty === null), startTime: this.receivedQuestionTime, title: question.question, type: question.type, video: question.video }, quizId: this.challengeData.kahoot.uuid, quizMaster: this.challengeData.challenge.quizMaster, quizTitle: this.challengeData.kahoot.title, quizType: this.challengeData.progress.quizType, sessionId: this.gameid, startTime: this.challengeData.progress.timestamp }; // small changes for specific types switch (question.type) { case "word_cloud": case "open_ended":{ Object.assign(payload.question.answers[0],{ originalText: text, text: text.toLowerCase().replace(/[~`!@#$%^&amp;*(){}[\];:"'&lt;,.>?/\\|\-_+=]/gm,"") }); payload.question.choices = []; break; } case "jumble":{ let f = choice; if(f.length !== 4){ f = [3,2,1,0]; } payload.question.answers[0].selectedJumbleOrder = f; break; } case "multiple_select_quiz":{ payload.question.answers[0].selectedChoices = choice; payload.question.answers[0].choiceIndex = -5; break; } case "content":{ Object.assign(payload.question.answers[0],{ choiceIndex: -2, isCorrect: true, reactionTime: 0 }); break; }} let oldScore = score; score += payload.question.answers[0].bonusPoints.answerStreakBonus; this.data.totalStreak += score - oldScore; this.data.totalScoreNoBonus += oldScore; this.data.totalScore += score; if(correct){this.data.correctCount++;} if(!correct &amp;&amp; empty === null){this.data.unansweredCount++;} if(!correct &amp;&amp; empty !== null){this.data.incorrectCount++;} const event = { choice, type: question.type, isCorrect: correct, text, receivedTime: Date.now(), pointsQuestion, points: score, correctAnswers: c, correctChoices: c2, totalScore: this.data.totalScore, rank: this._getRank(), nemesis: this._getNemesis(), pointsData: { questionPoints: oldScore, totalPointsWithBonuses: this.data.totalScore, totalPointsWithoutBonuses: this.data.totalScoreNoBonus, answerStreakPoints: { streakLevel: (correct &amp;&amp; this.data.streak) || 0, streakBonus: this._calculateStreakBonus(), totalStreakPoints: this.data.totalStreak, previousStreakLevel: oldstreak, previousStreakBonus: this._calculateStreakBonus(oldstreak) } } }; this.data.finalResult = { rank: event.rank, cid: this.cid, correctCount: this.data.correctCount, incorrectCount: this.data.incorrectCount, unansweredCount: this.data.unansweredCount, isKicked: false, isGhost: false, playerCount: this.challengeData.challenge.challengeUsersList.length + 1, startTime: this.challengeData.progress.timestamp, quizId: this.challengeData.kahoot.uuid, name: this.name, totalScore: this.data.totalScore, hostId: "", challengeId: "", isOnlyNonPointGameBlockKahoot: false }; return this._httpRequest(`https://kahoot.it/rest/challenges/${this.challengeData.challenge.challengeId}/answers`,{ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(JSON.stringify(payload)) }, method: "POST" },false,JSON.stringify(payload)).then((data)=>{ if(empty === null){ return event; } this.ti = setTimeout(()=>{ this._emit("TimeOver"); this._emit("QuestionEnd",event); this.next(); },1000); return; }); }; /** * next - Go to the next part (challenge) * * @function Client#next */ this.next = ()=>{ if(this.stop){ return; } switch (this.data.phase) { case "start": { this.data.phase = "ready"; const kahoot = this.challengeData.kahoot; let qqa = []; for(let question of kahoot.questions){ qqa.push(question.choices ? question.choices.length : null); } this._emit("QuizStart",{ name: kahoot.title, quizQuestionAnswers: qqa }); setTimeout(()=>{this.next();},5000); break; } case "ready": { this._getProgress(this.data.questionIndex).then((inf)=>{ if(this.data.hitError === true){this.data.hitError = false;} if(Object.keys(inf).length !== 0){ this.challengeData.progress = inf; } this.data.phase = "answer"; let q = this.challengeData.kahoot.questions[this.data.questionIndex]; this._emit("QuestionReady",Object.assign(q,{ questionIndex: this.data.questionIndex, timeLeft: 5, gameBlockType: q.type, gameBlockLayout: q.layout, quizQuestionAnswers: this.quiz.quizQuestionAnswers })); setTimeout(()=>{this.next();},5000); }).catch((err)=>{ if(this.data.hitError){ // Assume something is terribly wrong. clearTimeout(this.ti); this.disconnectReason = "Kahoot - Internal Server Error"; this.socket.close(); } this.data.hitError = true; this.next(); }); break; } case "answer": { let q = this.challengeData.kahoot.questions[this.data.questionIndex]; this.receivedQuestionTime = Date.now(); this.data.phase = "leaderboard"; if(!q){ this.disconnectReason = "Unknown Error"; this.socket.close(); return; } if(q.type === "content"){ this.data.questionIndex++; this.data.phase = "ready"; if(this.data.questionIndex === this.challengeData.kahoot.questions.length){ this.data.phase = "close"; } if(this.challengeData.challenge.game_options.question_timer &amp;&amp; !this.defaults.options.ChallengeWaitForInput){ setTimeout(()=>{ this.next(); },10000); } return; } if(this.challengeData.challenge.game_options.question_timer &amp;&amp; !this.defaults.options.ChallengeWaitForInput){ this.ti = setTimeout(async()=>{ const evt = await this.answer(null,null); if(q.type !== "content"){ this._emit("TimeOver"); this._emit("QuestionEnd",evt); } this.next(); },q.time || 5000); this._emit("QuestionStart",Object.assign(q,{ questionIndex: this.data.questionIndex, gameBlockType: q.type, gameBlockLayout: q.layout, quizQuestionAnswers: this.quiz.quizQuestionAnswers, timeAvailable: q.time })); return; } this._emit("QuestionStart",Object.assign(q,{ questionIndex: this.data.questionIndex, gameBlockType: q.type, gameBlockLayout: q.layout, quizQuestionAnswers: this.quiz.quizQuestionAnswers, timeAvailable: q.time })); // else, you must do everything yourself. break; } case "leaderboard":{ this.data.questionIndex++; this.data.phase = "ready"; if(this.data.questionIndex === this.challengeData.kahoot.questions.length){ this.data.phase = "close"; if(this.defaults.options.ChallengeAutoContinue){ setTimeout(()=>{this.next();},5000); } return; } if(this.defaults.options.ChallengeAutoContinue){ setTimeout(()=>{this.next();},5000); } break; } case "close":{ this.data.phase = "complete"; this._emit("QuizEnd",this.data.finalResult); this._emit("Podium",{ podiumMedalType: ["gold","silver","bronze"][this._getRank() - 1] }); if(this.defaults.options.ChallengeAutoContinue){ setTimeout(()=>{ this.next(); },30000); } break; } case "complete":{ this.stop = true; this.disconnectReason = "Session Ended"; this.socket.close(); } } }; this.leave = ()=>{ this.stop = true; this.disconnectReason = "Player Left"; this.socket.close(); }; const joined = (cid)=>{ setTimeout(()=>{this.socket.emit("message",JSON.stringify([{ channel: "/service/controller", data: { type: "loginResponse", cid: cid+"" } }]));}); this._send = async ()=>{throw "This error should not appear unless you are trying to do something silly.";}; }; this._calculateStreakBonus = (i)=>{ let info = i || this.data.streak; if(this.defaults.options.ChallengeUseStreakBonus){ if(info >= 6){ return 500; }else if(info > 0){ return (info - 1) * 100; }else{ return 0; } }else{ return 0; } }; this._send = async (m)=>{ if(m.data &amp;&amp; m.data.type === "login"){ this.name = m.data.name + ""; /** * @since 2.0.0 * Removed reconnecting - Client id not provided. */ return this._httpRequest(`https://kahoot.it/rest/challenges/${this.challengeData.challenge.challengeId}/join/?nickname=${encodeURIComponent(this.name)}`,{ method: "POST" },true).then((data)=>{ if(data.error){ throw data; } Object.assign(this.challengeData,data); this.cid = data.playerCid; joined(this.cid); if(this.defaults.options.ChallengeAutoContinue){ setTimeout(()=>{this.next();},5000); } return this.challengeData; }); } }; this._httpRequest = (url,opts,json,packet)=>{ return new Promise((resolve, reject)=>{ const handleRequest = (res)=>{ const chunks = []; res.on("data",(chunk)=>{ chunks.push(chunk); }); res.on("end",()=>{ const data = Buffer.concat(chunks).toString("utf8"); if(this.loggingMode){ console.log("RECV: " + data); } if(json){ try{ resolve(JSON.parse(data)); }catch(e){ reject(data); } }else{ resolve(data); } }); }; const parsed = new URL(url); let options = { headers: { "User-Agent": this.userAgent, "Origin": "kahoot.it", "Referer": "https://kahoot.it/", "Accept-Language": "en-US,en;q=0.8", "Accept": "*/*" }, host: parsed.hostname, protocol: parsed.protocol, path: parsed.pathname + (parsed.search || "") }; for(let i in opts){ if(typeof opts[i] === "object"){ if(!options[i]){ options[i] = opts[i]; }else{ Object.assign(options[i],opts[i]); } }else{ options[i] = opts[i]; } } const proxyOptions = this.defaults.proxy(options); if(proxyOptions &amp;&amp; typeof proxyOptions.destroy === "function"){ // assume proxyOptions is a request object proxyOptions.on("request",handleRequest); return; }else if(proxyOptions &amp;&amp; typeof proxyOptions.then === "function"){ // assume Promise&lt;IncomingMessage> proxyOptions.then((req)=>{ req.on("request",handleRequest); }); return; } options = proxyOptions || options; if(this.loggingMode){ console.log("SEND: " + JSON.stringify({ options, packet })); } let req; if(options.protocol === "https:"){ req = https.request(options,handleRequest); }else{ req = http.request(options,handleRequest); } req.on("error",(e)=>{reject(e);}); req.end(packet); }); }; this._getNemesis = ()=>{ if(!this.challengeData.progress.playerProgress){ return null; } const scores = Array.from(this.challengeData.progress.playerProgress.playerProgressEntries); const latest = scores.reverse()[0].questionMetrics; let rank = 0; let name = null; let totalScore = null; for(let i in latest){ if(i === this.name){ continue; } if(latest[i] >= this.data.totalScore){ rank++; if(latest[i] &lt; totalScore || name === null){ name = i; totalScore = latest[i]; } } } if(rank){ return { name, isGhost: false, rank, totalScore }; } }; this._getRank = ()=>{ const nem = this._getNemesis(); if(nem){ return nem.rank + 1; }else{ return 1; } }; this._getProgress = (q)=>{ if(typeof q !== "undefined"){ return this._httpRequest(`https://kahoot.it/rest/challenges/${this.challengeData.challenge.challengeId}/progress/?upToQuestion=${q}`,null,true); }else{ return this._httpRequest(`https://kahoot.it/rest/challenges/pin/${this.gameid}`,null,true).then((data)=>{ return this._httpRequest(`https://kahoot.it/rest/challenges/${data.challenge.challengeId}/progress`,null,true).then((data2)=>{ return Object.assign(data,{progress:data2}); }); }); } }; this._getProgress().then(inf=>{ if(Object.keys(inf.progress).length == 0){ this.disconnectReason = "Invalid Challenge"; return this.socket.close(); } this.challengeData = inf; if(inf.challenge.endTime &lt;= Date.now() || inf.challenge.challengeUsersList.length >= inf.challenge.maxPlayers){ this.disconnectReason = "Challenge Ended/Full"; return this.socket.close(); }else{ this.emit("HandshakeComplete"); } }); } // pretends to be a websocket class ChallengeHandler extends EventEmitter{ constructor(client,content){ super(); client.challengeData = content; Injector.call(client); this.readyState = 3; this.close = ()=>{ this.stop = true; clearTimeout(client.ti); this.emit("close"); }; } } module.exports = ChallengeHandler; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Namespaces</h3><ul><li><a href="Client_data.html">data</a></li><li><a href="Client_defaults.html">defaults</a></li><li><a href="Client_defaults.modules.html">modules</a></li><li><a href="Client_defaults.options.html">options</a></li><li><a href="Client_defaults.wsproxy.WsProxyReturn.html">WsProxyReturn</a></li><li><a href="Client_quiz.html">quiz</a></li><li><a href="Client_quiz.currentQuestion.html">currentQuestion</a></li><li><a href="LiveEventTimetrack.html">LiveEventTimetrack</a></li><li><a href="Nemesis.html">Nemesis</a></li><li><a href="PointsData.html">PointsData</a></li><li><a href="StreakPoints.html">StreakPoints</a></li></ul><h3>Classes</h3><ul><li><a href="Client.html">Client</a></li></ul><h3>Events</h3><ul><li><a href="Client.html#event:Disconnect">Disconnect</a></li><li><a href="Client.html#event:Feedback">Feedback</a></li><li><a href="Client.html#event:GameReset">GameReset</a></li><li><a href="Client.html#event:Joined">Joined</a></li><li><a href="Client.html#event:NameAccept">NameAccept</a></li><li><a href="Client.html#event:Podium">Podium</a></li><li><a href="Client.html#event:QuestionEnd">QuestionEnd</a></li><li><a href="Client.html#event:QuestionReady">QuestionReady</a></li><li><a href="Client.html#event:QuestionStart">QuestionStart</a></li><li><a href="Client.html#event:QuizEnd">QuizEnd</a></li><li><a href="Client.html#event:QuizStart">QuizStart</a></li><li><a href="Client.html#event:RecoveryData">RecoveryData</a></li><li><a href="Client.html#event:TeamAccept">TeamAccept</a></li><li><a href="Client.html#event:TeamTalk">TeamTalk</a></li><li><a href="Client.html#event:TimeOver">TimeOver</a></li><li><a href="Client.html#event:TwoFactorCorrect">TwoFactorCorrect</a></li><li><a href="Client.html#event:TwoFactorReset">TwoFactorReset</a></li><li><a href="Client.html#event:TwoFactorWrong">TwoFactorWrong</a></li></ul><h3>Global</h3><ul><li><a href="global.html#EventEmitter">EventEmitter</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.5</a> on Sun Oct 11 2020 19:45:43 GMT-0700 (Pacific Daylight Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>