kahoot.js-latest
Version:
Answer, Join Kahoot Quizzes with nodejs
734 lines (700 loc) • 23.5 kB
HTML
<!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>/**
* 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 < 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 = /[~`!@#$%^&*(){}[\];:"'<,.>?/\\|\-_+=]/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(/[~`!@#$%^&*(){}[\];:"'<,.>?/\\|\-_+=]/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 && empty === null){this.data.unansweredCount++;}
if(!correct && 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 && 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 && !this.defaults.options.ChallengeWaitForInput){
setTimeout(()=>{
this.next();
},10000);
}
return;
}
if(this.challengeData.challenge.game_options.question_timer && !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 && 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 && typeof proxyOptions.destroy === "function"){
// assume proxyOptions is a request object
proxyOptions.on("request",handleRequest);
return;
}else if(proxyOptions && typeof proxyOptions.then === "function"){
// assume Promise<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] < 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 <= 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>