Browse Source

Redirect on POST. Recent question page. Many leaderboard/bonusslot fixes. Twitch Login example pages in public/

master
Brandon Cornejo 7 months ago
parent
commit
38178b3d67
13 changed files with 501 additions and 120 deletions
  1. +4
    -0
      bot.js
  2. +8
    -0
      pg_setup.sql
  3. +148
    -0
      public/landing.html
  4. +18
    -0
      public/login.html
  5. +30
    -13
      src/db.js
  6. +93
    -30
      src/quiz.js
  7. +46
    -24
      src/redis-brain.js
  8. +9
    -15
      src/twitch.js
  9. +52
    -17
      src/web.js
  10. +12
    -9
      templates/base.html
  11. +33
    -6
      templates/index.html
  12. +10
    -6
      templates/leaderboard.html
  13. +38
    -0
      templates/recent.html

+ 4
- 0
bot.js View File

@@ -1,3 +1,7 @@
// Re-set state on startup
const quiz = require('./src/quiz');
quiz.set_bot_state();

// Start twitch bot
const twitch = require('./src/twitch');
twitch.start_bot();


+ 8
- 0
pg_setup.sql View File

@@ -9,6 +9,13 @@ CREATE TABLE questions (
total_correct int,
question text UNIQUE,
answer text,
bonus_slot_1 text,
bonus_slot_2 text,
bonus_slot_3 text,
answer_a_total int,
answer_b_total int,
answer_c_total int,
answer_d_total int,
ended_at TIMESTAMP NOT NULL DEFAULT NOW()
);

@@ -24,5 +31,6 @@ CREATE TABLE answers (
viewer_id int REFERENCES viewers(id),
question_id int REFERENCES questions(id),
answer text,
points int,
PRIMARY KEY (id, viewer_id, question_id)
);

+ 148
- 0
public/landing.html View File

@@ -0,0 +1,148 @@
<html>
<body>
<div><h1>Hi, <span id="username_display">[User]</span>,</h1></div>
<div>Your email address is <span id="email_display">[Email]</span>.</div>
<div>Your email address <strong id="verified_display" style="text-decoration: underline;">[Verified]</strong> been verified.</div>

<hr/>

<div><h1>TwitchTrivia Data</h1></div>
<div><a id="direct_link">Direct Link</a></div>
<div style="display: flex;">
<div style="padding-right: 40px;">
<h4>Leaderboard</h4>
<table>
<thead><th>User</th><th>Points</th></thead>
<tbody id="leaderboard_display">
</tbody>
</table>
</div>

<div style="padding-left: 40px;">
<h4>Stats</h4>
<table>
<tbody id="stats_display">
</tbody>
</table>
</div>
</div>
</body>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
refreshData();

let rid = window.setInterval(refreshData, 20000);
});

function refreshData() {
/* Parse out the JWT returned by Twitch OIDC login for user data */
let token_raw = document.location.hash.slice(10);
let token = JSON.parse(atob(token_raw.split('.')[1]));
let username = token.preferred_username;
let email = token.email;
let verified = token.email_verified;

console.log(username, email, verified);

document.querySelector('#username_display').innerText = username;
document.querySelector('#email_display').innerText = email;
document.querySelector('#verified_display').innerText = verified ? 'has' : 'hasn\'t';

document.querySelector('#direct_link').href = window.location.origin + `/leaderboard/${username}`;


/* Call TwitchTrivia for user data */
fetch(window.location.origin + `/leaderboard/${username}?format=json`)
.then(response => response.json())
.then(data => {
populateLeaderboardDisplay(data, username);
}).catch((error) => {
console.error('Error fetching from TwitchTrivia', error);
});
}

function clearTableData(tbody) {
while(tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
};

function populateLeaderboardDisplay(data, username) {
let tbody = document.querySelector('#leaderboard_display');
clearTableData(tbody);
for(let leader of data.leaderboard) {
// Create text nodes out of the data
let name = document.createTextNode(leader.username);
let points = document.createTextNode(leader.points);

// Add them to the table
let tr = tbody.insertRow();
tr.insertCell().appendChild(name);
tr.insertCell().appendChild(points);

if(leader.username == username) {
tr.style = "color: green; text-decoration: underline;";
}
}

let tr = null;
tbody = document.querySelector('#stats_display');
clearTableData(tbody);

if(data.active && data.active.active_question) {
// Title
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("CURRENT QUESTION"));

// Q
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Question"));
tr.insertCell().appendChild(document.createTextNode(data.active.active_question));

// Bonus Slots
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("First to Answer"));
tr.insertCell().appendChild(document.createTextNode(data.active.bonus_slot_1 || "AVAILABLE!"));
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Second to Answer"));
tr.insertCell().appendChild(document.createTextNode(data.active.bonus_slot_2 || "AVAILABLE!"));
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Third to Answer"));
tr.insertCell().appendChild(document.createTextNode(data.active.bonus_slot_3 || "AVAILABLE!"));

// Percents
if(data.active.answer_percents) {
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Answered A"));
tr.insertCell().appendChild(document.createTextNode(`${data.active.answer_percents.A || 0}% (${data.active.answer_totals.A || 0})`));

tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Answered B"));
tr.insertCell().appendChild(document.createTextNode(`${data.active.answer_percents.B || 0}% (${data.active.answer_totals.B || 0})`));

tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Answered C"));
tr.insertCell().appendChild(document.createTextNode(`${data.active.answer_percents.C || 0}% (${data.active.answer_totals.C || 0})`));

tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Answered D"));
tr.insertCell().appendChild(document.createTextNode(`${data.active.answer_percents.D || 0}% (${data.active.answer_totals.D || 0})`));
}
}


tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("OVERALL"));

// Total Votes
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Total Votes"));
tr.insertCell().appendChild(document.createTextNode(data.combined_total_responses));

// Total Correct
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Total Correct Votes"));
tr.insertCell().appendChild(document.createTextNode(data.combined_total_correct));
}
</script>
</html>

+ 18
- 0
public/login.html View File

@@ -0,0 +1,18 @@
<html>
<body>
<button onclick=twitchLogin()>Twitch Login</button>
</body>
<script>
function twitchLogin() {
let client_id = "l8oqrowcuszvkbxd84243qmhn86o22";
let redirect_uri = window.location.origin + "/landing.html";
let response_type = "id_token";
let scope = "openid user:read:email";
let claims = JSON.stringify({"id_token": {"email": null, "email_verified": null, "preferred_username": null}});

let twitch_url = `https://id.twitch.tv/oauth2/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&claims=${claims}`;
console.log(twitch_url);
window.location.href = twitch_url;
}
</script>
</html>

+ 30
- 13
src/db.js View File

@@ -14,20 +14,22 @@ pool.on('error', (err, client) => {
});

const config = {
query_create_question: "INSERT INTO questions(question, answer, total_responses, total_valid, total_correct, ended_at) VALUES($1, $2, $3, $4, $5, $6) RETURNING id",
query_create_viewer: "INSERT INTO viewers(username, points) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET points = EXCLUDED.points RETURNING id, username",
query_create_answer: "INSERT INTO answers(viewer_id, question_id, answer) VALUES ($1, $2, $3) RETURNING id",
query_create_question: "INSERT INTO questions(question, answer, total_responses, total_valid, total_correct, bonus_slot_1, bonus_slot_2, bonus_slot_3, answer_a_total, answer_b_total, answer_c_total, answer_d_total, ended_at) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id",
query_create_viewer: "INSERT INTO viewers(username, points) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET points = viewers.points + EXCLUDED.points RETURNING id, username",
query_create_answer: "INSERT INTO answers(viewer_id, question_id, answer, points) VALUES ($1, $2, $3, $4) RETURNING id",

query_all_question_data: "SELECT * FROM questions",
query_all_viewer_data: "SELECT viewers.*, (SELECT Count(*) FROM answers WHERE viewer_id = viewers.id) as total_answers FROM viewers",
query_viewer_data: "SELECT viewers.*, (SELECT Count(*) FROM answers WHERE viewer_id = viewers.id) as total_answers FROM viewers WHERE viewers.username = $1",
query_all_answer_data: "SELECT v.username, a.answer, q.question, CASE a.answer WHEN q.answer THEN true ELSE false END FROM answers a LEFT JOIN viewers v ON a.viewer_id = v.id LEFT JOIN questions q ON a.question_id = q.id",
query_all_answer_data: "SELECT v.username, a.answer, q.question, a.points, CASE a.answer WHEN q.answer THEN true ELSE false END as correct FROM answers a LEFT JOIN viewers v ON a.viewer_id = v.id LEFT JOIN questions q ON a.question_id = q.id",

query_top10_data: "SELECT username, points FROM viewers ORDER BY points DESC LIMIT 10",
query_top10_data: "SELECT username, points FROM viewers ORDER BY points DESC LIMIT 20",
query_totals_data: "SELECT count(id) as total_questions, sum(total_responses) as total_responses, sum(total_valid) as total_valid, sum(total_correct) as total_correct FROM questions",

query_recent_questions: "SELECT * FROM questions ORDER BY ended_at DESC LIMIT 20",
};

console.log("* [PSQL] Hooked into Postgres backend");
console.log("* [psql] Hooked into Postgres backend");

var self = module.exports = {
query: function(querystr, values=undefined) {
@@ -35,7 +37,7 @@ var self = module.exports = {
pool.query(querystr, values).then((data) => {
return resolve(data.rows);
}).catch((error) => {
console.log("* [PSQL] Error performing query", querystr, error);
console.log("* [psql] Error performing query", querystr, error);
});
});
},
@@ -50,10 +52,17 @@ var self = module.exports = {
});
},
get_viewer_data: function(username) {
return new Promise((resolve) => {
return new Promise((resolve) => {
self.query(config.query_viewer_data, [username]).then((viewer) => {
resolve(viewer);
});
});
},
get_recent_questions: function() {
return new Promise((resolve) => {
self.query(config.query_recent_questions).then((recents) => {
resolve(recents);
});
});
},
get_all_question_data: function() {
@@ -61,7 +70,7 @@ var self = module.exports = {
self.query(config.query_all_question_data).then((questions) => {
resolve(questions);
}).catch((error) => {
console.log("* [Psql] Error gathering all question data", error);
console.log("* [psql] Error gathering all question data", error);
});
});
},
@@ -86,10 +95,18 @@ var self = module.exports = {
q.total_responses,
q.total_valid,
q.total_correct,
q.bonus_slot_1,
q.bonus_slot_2,
q.bonus_slot_3,
q.answer_totals['A'] || null,
q.answer_totals['B'] || null,
q.answer_totals['C'] || null,
q.answer_totals['D'] || null,
new Date()
];

client.query(config.query_create_question, question_values).then((dbq) => {
console.log(`* [Psql] Created new question in Postgres with id ${dbq.rows[0].id}`);
console.log(`* [psql] Created new question in Postgres with id ${dbq.rows[0].id}`);
const question_id = dbq.rows[0].id;

// If there weren't any answers for this question, our job is done
@@ -101,7 +118,7 @@ var self = module.exports = {
// Then insert/update all of the viewers
const viewer_inserts = Object.entries(q.active_points).map((viewer) => {
return client.query(config.query_create_viewer, [viewer[0], viewer[1]]).then((dbv) => {
console.log(`* [psql] Upserted viewer data: ${viewer[0]} = ${dbv.rows[0].id}`);
console.info(`* [psql] Upserted viewer data: ${viewer[0]} = ${dbv.rows[0].id}`);
return dbv.rows[0];
}).catch((error) => {
console.log(`* [psql] Error upserting viewer ${viewer}`, error);
@@ -117,9 +134,9 @@ var self = module.exports = {

// Now insert all of the viewers answers to this question
const answer_inserts = Object.entries(q.active_votes).map((answer) => {
let answer_value = [viewer_id_map[answer[0]], question_id, answer[1]]
let answer_value = [viewer_id_map[answer[0]], question_id, answer[1], (q.active_points[answer[0]] || 0)];
return client.query(config.query_create_answer, answer_value).then((dba) => {
console.log(`* [psql] Inserted answer data: ${answer} = ${dba.rows[0].id}`);
console.info(`* [psql] Inserted answer data: ${answer} = ${dba.rows[0].id}`);
return dba.rows[0];
}).catch((error) => {
console.log(`* [psql] Error upserting answer ${answer}`, error);


+ 93
- 30
src/quiz.js View File

@@ -4,6 +4,8 @@ const redis = require('./redis-brain');
const valid_answers = ['A', 'B', 'C', 'D'];
const bonus_slot_points = [100, 50, 25];
const standard_points = [20];
const leaderboard_max = 10;
const personal_leaderboard_max = 10;

var self = module.exports = {
bonus_slot_1: null,
@@ -13,21 +15,25 @@ var self = module.exports = {
endCurrentQuestion: function() {
return new Promise((resolve) => {
redis.getActiveQuestionData(false).then((question_data) => {
console.log(`* [quiz] We got question data: ${JSON.stringify(question_data)}`);
console.log(`* [quiz] Ending the current question`);

// Send all the data off to be saved in the database
db.write_question_data(question_data).catch((error) => {
console.log("* [Quiz] Error writing question data to Postgres");
console.error("* [Quiz] Error writing question data to Postgres");
});

redis.clearActiveQuestion().then(() => {
self.bonus_slot_1 = null;
self.bonus_slot_2 = null;
self.bonus_slot_3 = null;

resolve();
}).catch((error) => {
console.log("* [Quiz] Error clearing active question");
console.error("* [Quiz] Error clearing active question");
});

}).catch((error) => {
console.log("* [quiz] Error getting active question data from redis");
console.error("* [quiz] Error getting active question data from redis");
});
});
},
@@ -37,7 +43,6 @@ var self = module.exports = {

// Weed out immediate faulty votes that aren't (A|B|C|D)
if(!(answer) || !(valid_answers.includes(answer))) {
console.log("We have an invalid answer:", answer);
redis.incrementResponseCounts(false, false);
return resolve();
}
@@ -55,7 +60,7 @@ var self = module.exports = {

// Either way, store their answer in redis (if they haven't already answered)
redis.addViewerVote(username, answer).catch((err) => {
console.log(`* [quiz] Error setting user vote for ${username}`);
console.error(`* [quiz] Error setting user vote for ${username}`);
});
});

@@ -66,9 +71,9 @@ var self = module.exports = {
if(nopoints) {
// If it was the wrong answer
redis.addViewerPoints(username, 0, null).then(() => {
console.log(`* [quiz] Assigning 0 points to ${username} for an incorrect answer`);
console.info(`* [quiz] Assigning 0 points to ${username} for an incorrect answer`);
}).catch((error) => {
console.log(`* [quiz] Error adding no-points for ${username}`, error);
console.error(`* [quiz] Error adding no-points for ${username}`, error);
});

return;
@@ -95,14 +100,14 @@ var self = module.exports = {
}

redis.addViewerPoints(username, points, used_slot).then(() => {
console.log(`* [quiz] Awarding ${points} points to ${username} for a correct first answer`);
console.info(`* [quiz] Awarding ${points} points to ${username} for a correct first answer`);
}).catch((error) => {
console.log(`* [quiz] Error adding ${points} for ${username}`, error);
console.error(`* [quiz] Error adding ${points} for ${username}`, error);
});
},
gatherLeaderboardData: function() {
gatherLeaderboardData: function(username=null) {
return new Promise((resolve) => {
redis.getActiveQuestionData(true).then((active) => {
redis.getActiveQuestionData(false).then((active) => {
if(active && active.active_points) {
// Sort viewers by points descending
active.active_points = Object.entries(active.active_points).sort((a, b) => {
@@ -132,24 +137,22 @@ var self = module.exports = {
data.combined_total_valid = parseInt(data.active.total_valid);
data.combined_total_correct = parseInt(data.active.total_correct);
data.combined_total_incorrect = (parseInt(data.active.total_valid) - parseInt(data.active.total_correct));

data.totals.total_responses = 0;
data.totals.total_valid = 0;
data.totals.total_correct = 0;
data.totals.total_responses = 0;
data.totals.total_valid = 0;
data.totals.total_correct = 0;
} else if(data.totals.total_responses) {
data.combined_total_responses = parseInt(data.totals.total_responses);
data.combined_total_valid = parseInt(data.totals.total_valid);
data.combined_total_correct = parseInt(data.totals.total_correct);
data.combined_total_incorrect = (parseInt(data.totals.total_valid) - parseInt(data.totals.total_correct));

data.active.total_responses = 0;
data.active.total_valid = 0;
data.active.total_correct = 0;
data.active.total_responses = 0;
data.active.total_valid = 0;
data.active.total_correct = 0;
}

// Supplement the database leaderboard with any points currently in redis
let leaderboard_names = data.leaderboard.map(x => x.username);
if(active.active_points) {
let leaderboard_names = data.leaderboard.map(x => x.username);
for(let voter of active.active_points) {
if(leaderboard_names.includes(voter[0])) {
for(let leader of data.leaderboard) {
@@ -162,25 +165,58 @@ var self = module.exports = {
}
}

// Re-order the leaderboard
data.leaderboard.sort((a, b) => {
return b.points - a.points;
});

// Trim it back down to 10 folks total
data.leaderboard = data.leaderboard.slice(0, 10);
}

// let leaderboard_names = data.leaderboard.map(x => x.username);
if(username && !leaderboard_names.includes(username)) {
user_data = db.get_viewer_data(username).then((ud) => {
if(ud.length) {
let found = false;
for(let ld of data.leaderboard) {
if(ld.username == ud[0].username) {
ld.points += ud[0].points;
found = true;
}
}

if(!found) {
data.leaderboard.push({username: ud[0].username, points: ud[0].points});
}
}

data.leaderboard = cleanLeaderboard(data.leaderboard, username=username);
delete data.active.active_points;
delete data.active.active_votes;
return resolve(data);
});
return;
}

data.leaderboard = cleanLeaderboard(data.leaderboard, username=username);
delete data.active.active_points;
delete data.active.active_votes;
resolve(data);
});
}).catch((error) => {
console.log("* [web] Error rendering the leaderboard page", error);
console.log("* [quiz] Error gathering the leaderboard data", error);
res.end();
});
});
},
gatherRecentQuestions: function() {
return new Promise((resolve) => {
db.get_recent_questions().then((data) => {
for(let q of data) {
q.answer_a_percent = Math.trunc((q.answer_a_total / q.total_valid) * 100);
q.answer_b_percent = Math.trunc((q.answer_b_total / q.total_valid) * 100);
q.answer_c_percent = Math.trunc((q.answer_c_total / q.total_valid) * 100);
q.answer_d_percent = Math.trunc((q.answer_d_total / q.total_valid) * 100);
}


resolve({recents: data});
});
});
},
generateFakeAnswerData: function() {
return new Promise((resolve) => {
function randomAnswer() {
@@ -206,4 +242,31 @@ var self = module.exports = {
self.handleViewerVote("fuzzyfirefly", randomAnswer());
});
},
set_bot_state: function() {
return new Promise((resolve) => {
redis.checkBonusSlotAvailability().then((bs_data) => {
self.bonus_slot_1 = bs_data[0];
self.bonus_slot_2 = bs_data[1];
self.bonus_slot_3 = bs_data[2];
});
});
},
};

function cleanLeaderboard(lb, username=null) {
// Re-order the leaderboard
lb.sort((a, b) => {
return b.points - a.points;
});

if(username) {
for(let i = (personal_leaderboard_max - 1); i < lb.length; i++) {
if(lb[i].username === username) {
lb[personal_leaderboard_max - 1] = lb[i];
}
}
}

// Trim it back down to 10 folks total
return lb.slice(0, username ? personal_leaderboard_max : leaderboard_max);
}

+ 46
- 24
src/redis-brain.js View File

@@ -13,6 +13,7 @@ const config = {
total_responses_key: "twitch_bot_total_responses",
total_valid_key: "twitch_bot_total_valid_answers",
total_correct_key: "twitch_bot_total_correct_answers",
answer_totals: "twitch_bot_answer_totals_hash",
};

client.on("error", function(error) {
@@ -47,6 +48,7 @@ var self = module.exports = {
.del(config.total_responses_key, config.output)
.del(config.total_valid_key, config.output)
.del(config.total_correct_key, config.output)
.del(config.answer_totals, config.output)
.exec(function(err, replies) {
resolve({cleared: true});
});
@@ -87,39 +89,41 @@ var self = module.exports = {
.get(config.total_responses_key)
.get(config.total_valid_key)
.get(config.total_correct_key)
.hgetall(config.answer_totals)
.exec(function(err, replies) {
var answer_totals = {};
var answer_percents = {};

if(replies[10]) {
answer_totals = Object.fromEntries(Object.entries(replies[10]).map(([k, v]) => [k, Number(v)]));
answer_percents = calculateAnswerPercents(answer_totals)
}
var answer_percents = replies[10] ? calculateAnswerPercents(replies[10]) : null;

// Turn array into a dict for easier visual parsing
if(trimmed) {
return resolve({
active_question: replies[0],
active_answer: replies[1],
bonus_slot_1: replies[2],
bonus_slot_2: replies[3],
bonus_slot_3: replies[4],
total_responses: replies[7],
total_valid: replies[8],
total_correct: replies[9],
total_incorrect: (replies[8] - replies[9]).toString(),
});
}

resolve({
let data = {
active_question: replies[0],
active_answer: replies[1],
bonus_slot_1: replies[2],
bonus_slot_2: replies[3],
bonus_slot_3: replies[4],
active_votes: replies[5],
active_points: replies[6],
total_responses: replies[7],
total_valid: replies[8],
total_correct: replies[9],
total_responses: replies[7] || 0,
total_valid: replies[8] || 0,
total_correct: replies[9] || 0,
total_incorrect: (replies[8] - replies[9]).toString(),
});
answer_totals: answer_totals,
answer_percents: answer_percents,
};

if(!trimmed) {
data.active_votes = replies[5];
data.active_points = replies[6];
}

resolve(data);
});
});
},

/*** Bot specific actions ***/
addViewerVote: function(username, answer) {
return new Promise((resolve) => {
@@ -130,7 +134,10 @@ var self = module.exports = {
console.log(`* [redis] Error setting a viewer vote: ${err}`);
return;
}
resolve();

client.hincrby(config.answer_totals, answer, 1, function(ret) {
resolve();
});
});
});
},
@@ -170,7 +177,7 @@ var self = module.exports = {
var correct = (submitted_answer == q.answer);
self.hasUserVoted(username).then((has_voted) => {
if(correct && has_voted) {
console.log(`* [redis] Answer invalided, user ${username} already voted`);
console.info(`* [redis] Answer invalided, user ${username} already voted`);
}
resolve((correct && !has_voted));
});
@@ -226,3 +233,18 @@ var self = module.exports = {
});
},
};

function calculateAnswerPercents(answer_totals) {
let total = Object.values(answer_totals).map(Number).reduce(function(total, answer) {
return total + answer;
});

let percents = {};

for([key, value] of Object.entries(answer_totals)) {
// percents[key] = Number.parseFloat(((Number(value)/total) * 100).toFixed(2));
percents[key] = Math.trunc((Number(value)/total) * 100);
}

return percents;
}

+ 9
- 15
src/twitch.js View File

@@ -19,7 +19,7 @@ const opts = {
};

// Create a client with our options
console.log("* [TWITCH] Bot starting up...");
console.log("* [twitch] Bot starting up...");
const client = new tmi.client(opts);

// Hook into redis brain
@@ -33,7 +33,7 @@ var self = module.exports = {

// Connect to Twitch:
client.connect();
console.log("* [TWITCH] Bot connected and listening");
console.log("* [twitch] Bot connected and listening");
}
};

@@ -59,8 +59,8 @@ function onMessageHandler (target, context, msg, self) {
function onWhisperHandler(target, context, msg) {
if (msg.startsWith('!submitfakes')) {
quiz.generateFakeAnswerData().then((resolve) => {
console.log("* [bot] Generating fake answer data for testing purposes");
}).catch((error) => { console.log("* [bot] Error attempting to generate fake data", error); });
console.log("* [twitch] Generating fake answer data for testing purposes");
}).catch((error) => { console.log("* [twitch] Error attempting to generate fake data", error); });
}

if (msg.startsWith('!submitfake ')) {
@@ -68,28 +68,22 @@ function onWhisperHandler(target, context, msg) {
let username = msg.slice(14);

quiz.handleViewerVote(username, answer).then((resolve) => {
console.log(`* [bot] Faked a vote of ${answer} from ${username} via whisper`);
console.log(`* [twitch] Faked a vote of ${answer} from ${username} via whisper`);
}).catch((error) => {
console.log(`* [bot] Error attempting to fake a vote via whisper`);
console.log(`* [twitch] Error attempting to fake a vote via whisper`);
});
}
}

function onChatHandler(target, context, msg) {
// If the command is known, let's execute it
if (msg === '!dice') {
const num = rollDice();
client.say(target, `You rolled a ${num}`);
console.log(`* Executed ${msg} command`);
} else if (msg.startsWith(opts.votecommand)) {
if (msg.startsWith(opts.votecommand)) {
var answer = msg.replace(opts.votecommand, '');
quiz.handleViewerVote(context.username, answer).then((resolve, reject) => {
console.log(`* [bot] Got a vote from ${context.username} (${answer.trim()})`);
// console.log(`* [bot] Got a vote from ${context.username} (${answer.trim()})`);
}).catch((err) => {
console.log(" [bot] Error handling viewer vote", err);
});
} else {
console.log(`* Unknown command ${msg}`);
}
}

@@ -101,6 +95,6 @@ function rollDice () {

// Called every time the bot connects to Twitch chat
function onConnectedHandler (addr, port) {
console.log(`* Connected to ${addr}:${port}`);
console.log(`* [twitch] Connected to ${addr}:${port}`);
}


+ 52
- 17
src/web.js View File

@@ -26,14 +26,17 @@ var self = module.exports = {
app.get('/', renderRoot);
app.post('/', handlePost);

app.get('/leaderboard', renderLeaderboard);

// API json responses
app.get('/questions', renderQuestions);
app.get('/viewers', renderViewers);
app.get('/viewer/:username', renderViewer);
app.get('/leaderboard/:username', renderPersonalLeaderboard);
app.get('/answers', renderAnswers);

app.get('/leaderboard', renderLeaderboard);
app.get('/recent', renderRecentQuestions);


// Debug endpoints
app.get('/redis', renderRedisView);

@@ -44,7 +47,7 @@ var self = module.exports = {
}

function renderRoot(req, res) {
redis.getActiveQuestionData().then((values) => {
redis.getActiveQuestionData(true).then((values) => {
if(values && values.active_points) {
// Sort viewers by points descending
values.active_points = Object.entries(values.active_points).sort((a, b) => {
@@ -59,6 +62,30 @@ function renderRoot(req, res) {
});
}

function handlePost(req, res) {
if(req.body.clear_question) {
console.log('* [web] Clearing the active question');
clearQuestion(req, res);
} else {
addQuestion(req, res);
}
}


function renderRecentQuestions(req, res) {
quiz.gatherRecentQuestions().then((data) => {
if(req.query.format == "json") {
res.json(data);
return;
}

res.render('recent.html', data=data);
}).catch((error) => {
console.log("* [web] Error rendering the recent questions page", error);
res.end();
});
}

function renderLeaderboard(req, res) {
quiz.gatherLeaderboardData().then((data) => {
if(req.query.format == "json") {
@@ -73,6 +100,23 @@ function renderLeaderboard(req, res) {
});
}

function renderPersonalLeaderboard(req, res) {
if(!req.params.username) {
res.json({"error": "No username provided"});
}

quiz.gatherLeaderboardData(req.params.username).then((data) => {
data.username = req.params.username;

if(req.query.format == "json") {
res.json(data);
return;
}

res.render('leaderboard.html', data);
});
}

function renderQuestions(req, res) {
db.get_all_question_data().then((questions) => {
res.json(questions);
@@ -90,8 +134,8 @@ function renderViewer(req, res) {
res.json({"error": "No username provided"});
}

db.get_viewer_data(req.params.username).then((viewers) => {
res.json(viewers);
db.get_viewer_data(req.params.username).then((viewer) => {
res.json(viewer);
});
}

@@ -101,27 +145,18 @@ function renderAnswers(req, res) {
});
}

function handlePost(req, res) {
if(req.body.clear_question) {
console.log('* [web] Clearing the active question');
clearQuestion(req, res);
} else {
addQuestion(req, res);
}
}

function clearQuestion(req, res) {
quiz.endCurrentQuestion().then((resolve, reject) => {
renderRoot(req, res);
res.redirect('/');
// renderRoot(req, res);
}).catch((error) => {
res.end();
});
}

function addQuestion(req, res) {
console.log(`* [web] New Question Submitted: ${req.body.new_question} - ${req.body.new_answer}`);
redis.setActiveQuestion(req.body.new_question, req.body.new_answer).then((resolve, reject) => {
renderRoot(req, res);
res.redirect('/');
}).catch((error) => {
res.end();
});


+ 12
- 9
templates/base.html View File

@@ -1,15 +1,18 @@
{% set navigation_bar = [
{href: '/', id: 'index', label: 'Active Question'},
{href: '/leaderboard', id: 'leaderboard', label: 'Leaderboard'}
{href: '/leaderboard', id: 'leaderboard', label: 'Leaderboard'},
{href: '/recent', id: 'recent', label: 'Recent Questions'}
] %}

{% set api_nav = [
{href: '/redis', label: 'Active Question'},
{href: '/leaderboard?format=json', label: 'Leaderboard'},
{href: '/questions', label: 'Questions'},
{href: '/viewers', label: 'Participants'},
{href: '/answers', label: 'Answers'},
{href: '/redis', label: 'Active Question'},
{href: '#', label: '/viewer/:username'}
{href: '/viewers', label: 'Participants'},
{href: '/recent', label: 'Recent Question Stats'},
{href: '/leaderboard/:username?format=json', label: 'User Leaderboard'},
{href: '/viewer/:username', label: 'User Data'}
] %}
{% set active_page = active_page|default('index') %}

@@ -17,9 +20,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/uikit.min.css" />
<script src="js/uikit.min.js"></script>
<script src="js/uikit-icons.min.js"></script>
<link rel="stylesheet" type="text/css" href="/css/uikit.min.css" />
<script src="/js/uikit.min.js"></script>
<script src="/js/uikit-icons.min.js"></script>

<title>Twitch Trivia</title>
</head>
@@ -52,8 +55,8 @@
{% block content %}
{% endblock %}
</div>
<div class="uk-margin uk-panel uk-text-center">
Created by <a href="https://binaryatrocity.name">binaryatrocity</a> - © 2020
<div class="uk-margin uk-panel uk-text-center uk-text-muted">
Created by <a class="uk-text-muted" href="https://binaryatrocity.name">binaryatrocity</a> - © 2020
</div>
</body>
</html>

+ 33
- 6
templates/index.html View File

@@ -20,24 +20,51 @@
<hr class="uk-divider-icon" />
<div class="uk-margin uk-flex uk-flex-around">
<div>
<div class="uk-text-emphasis uk-text-bold">Total Responses</div>
<div class="uk-text-bold">Total Responses</div>
<div class="uk-text-center">{{ total_responses }}</div>
</div>
<div>
<div class="uk-text-emphasis uk-text-bold">Total Valid</div>
<div class="uk-text-bold">Total Valid</div>
<div class="uk-text-center">{{ total_valid }}</div>
</div>
<div>
<div class="uk-text-emphasis uk-text-bold">Total Correct</div>
<div class="uk-text-bold">Total Correct</div>
<div class="uk-text-center">{{ total_correct }}</div>
</div>
<div>
<div class="uk-text-emphasis uk-text-bold">Total Incorrect</div>
<div class="uk-text-bold">Total Incorrect</div>
<div class="uk-text-center">{{ total_incorrect }}</div>
</div>
</div>
<hr class="uk-divider-icon" />
<div class="uk-margin">
{% if answer_percents %}
<div class="uk-margin uk-flex uk-flex-around">
{% for ap in answer_percents | dictsort %}
<div class="uk-text-center">
<div class="uk-text-bold">{{ ap[0] }}</div>
<div class="{% if ap[0] == active_answer %}uk-text-warning{% endif %}">{{ ap[1] }}% ({{ answer_totals[ap[0]] }})</div>
</div>
{% endfor %}
</div>
<hr class="uk-divider-icon" />
{% endif %}
<div class="uk-margin uk-flex uk-flex-around">
<div class="uk-text-center">
<div class="uk-text-bold">1st</div>
<div>{{ bonus_slot_1 or "Open" }}</div>
</div>
<div class="uk-text-center">
<div class="uk-text-bold">2nd</div>
<div>{{ bonus_slot_2 or "Open" }}</div>
</div>
<div class="uk-text-center">
<div class="uk-text-bold">3rd</div>
<div>{{ bonus_slot_3 or "Open"}}</div>
</div>
</div>
<hr class="uk-divider-icon" />

<div class="uk-margin" hidden>
<table class="uk-table">
<caption>Current Votes by Viewer</caption>
<thead>
@@ -65,7 +92,7 @@
<form method="post">
<div class="uk-margin">
<label for="new_question">Question:</label>
<input name="new_question" class="uk-input" type="text">
<input name="new_question" class="uk-input" type="text" required>
</div>

<div class="uk-margin">


+ 10
- 6
templates/leaderboard.html View File

@@ -4,8 +4,12 @@
{% block content %}
<div class="uk-panel uk-width-1-2">
<h1>Leaderboard Data</h1>
<div class="uk-margin">
<h3>Overall Totals</h3>
{% if username %}
<h2 class="uk-text-muted uk-margin-small-top">{{ username }}</h2>
{% endif %}

<div class="uk-margin uk-margin-large-top">
<h3 class="uk-text-center">All Questions</h3>
<div class="uk-margin uk-flex uk-flex-around">
<div>
<div class="uk-text-emphasis uk-text-bold">Responses</div>
@@ -29,7 +33,7 @@

{% if active %}
<div class="uk-margin">
<h3>Active Question Totals</h3>
<h3 class="uk-text-center">Current Question</h3>
<div class="uk-margin uk-flex uk-flex-around">
<div>
<div class="uk-text-emphasis uk-text-bold">Responses</div>
@@ -52,7 +56,7 @@
<hr class="uk-divider-icon" />
{% endif %}

<div class="uk-margin">
<div class="uk-margin" hidden>
<h3>Historical Question Totals</h3>
<div class="uk-margin uk-flex uk-flex-around">
<div>
@@ -73,7 +77,7 @@
</div>
</div>
</div>
<hr class="uk-divider-icon" />
<hr class="uk-divider-icon" hidden/>

<div class="uk-margin">
<table class="uk-table">
@@ -87,7 +91,7 @@
<tbody>
{% if leaderboard %}
{% for viewer in leaderboard %}
<tr>
<tr {% if username and username == viewer.username %}class="uk-text-success"{% endif %}>
<td>{{ viewer.username }}</td>
<td>{{ viewer.points }}</td>
</tr>


+ 38
- 0
templates/recent.html View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% set active_page = "recent" %}

{% block content %}
<div class="uk-panel uk-width-1-1">
<div class="uk-margin">
<table class="uk-table uk-table-middle uk-table-hover">
<caption>Recent Questions</caption>
<thead>
<tr>
<th>Question</th>
<th>Answer</th>
<th>Total Responses</th>
<th>Total Valids</th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
</tr>
</thead>
<tbody>
{% for recent in recents %}
<tr>
<td title="{{recent.question}}" class="uk-text-truncate">{{ recent.question }}</td>
<td>{{ recent.answer }}</td>
<td>{{ recent.total_responses }}</td>
<td>{{ recent.total_valid }}</td>
<td>{{ recent.answer_a_percent}}% ({{ recent.answer_a_total }})</td>
<td>{{ recent.answer_b_percent}}% ({{ recent.answer_b_total }})</td>
<td>{{ recent.answer_c_percent}}% ({{ recent.answer_c_total }})</td>
<td>{{ recent.answer_d_percent}}% ({{ recent.answer_d_total }})</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Loading…
Cancel
Save