diff --git a/client/api-docs/set-list.html b/client/api-docs/set-list.html
index 4bf2c70dc..cf8fe9f53 100644
--- a/client/api-docs/set-list.html
+++ b/client/api-docs/set-list.html
@@ -59,7 +59,7 @@
-
+
-
+
GET qbreader.org/api/set-list
@@ -137,8 +134,18 @@
Parameters
Returns
- An array of all the sets in the database. : string[]
+ A JSON object with the following properties:
+
diff --git a/client/database/index.js b/client/database/index.js
index 285d62085..23a8c8e3a 100644
--- a/client/database/index.js
+++ b/client/database/index.js
@@ -480,7 +480,7 @@ function QueryForm() {
const [bonusPaginationShift, setBonusPaginationShift] = React.useState(0);
const [queryTime, setQueryTime] = React.useState(0);
React.useEffect(() => {
- fetch('/api/set-list').then(response => response.json()).then(data => {
+ fetch('/api/set-list').then(response => response.json()).then(data => data.setList).then(data => {
document.getElementById('set-list').innerHTML = data.map(setName => `
${setName} `).join('');
});
}, []);
diff --git a/client/database/index.jsx b/client/database/index.jsx
index 44ce7dd66..522f8ef14 100644
--- a/client/database/index.jsx
+++ b/client/database/index.jsx
@@ -555,6 +555,7 @@ function QueryForm() {
React.useEffect(() => {
fetch('/api/set-list')
.then(response => response.json())
+ .then(data => data.setList)
.then(data => {
document.getElementById('set-list').innerHTML = data.map(setName => `
${setName} `).join('');
});
diff --git a/client/multiplayer/index.js b/client/multiplayer/index.js
index e4e804de5..ae040d1e6 100644
--- a/client/multiplayer/index.js
+++ b/client/multiplayer/index.js
@@ -12,25 +12,28 @@ document.getElementById('form').addEventListener('submit', (event) => {
fetch('/api/multiplayer/room-list')
.then(response => response.json())
- .then(rooms => {
- rooms = Object.entries(rooms);
- rooms.sort((a, b) => {
- if (a[1][1] === b[1][1]) {
- return b[1][0] - a[1][0];
+ .then(data => {
+ const { roomList } = data;
+ roomList.sort((a, b) => {
+ if (a.onlineCount === b.onlineCount) {
+ return b.playerCount - a.playerCount;
} else {
- return b[1][1] - a[1][1];
+ return b.onlineCount - a.onlineCount;
}
});
- return rooms;
+ return roomList;
})
- .then(rooms => {
- rooms.forEach(room => {
- const [roomName, [playerCount, onlineCount, isPermanent]] = room;
+ .then(roomList => {
+ roomList.forEach(room => {
+ const { roomName, playerCount, onlineCount, isPermanent } = room;
+
+ const a = document.createElement('a');
+ a.href = `/multiplayer/${encodeURIComponent(roomName)}`;
+ a.textContent = roomName;
+
const li = document.createElement('li');
- li.innerHTML = `
-
${escapeHTML(roomName)}
- - ${playerCount} player${playerCount === 1 ? '' : 's'} - ${onlineCount} online
- `;
+ li.appendChild(a);
+ li.appendChild(document.createTextNode(` - ${playerCount} player${playerCount === 1 ? '' : 's'} - ${onlineCount} online`));
li.classList.add('list-group-item');
if (isPermanent) {
@@ -42,9 +45,10 @@ fetch('/api/multiplayer/room-list')
});
fetch('/api/random-name')
- .then(res => res.text())
- .then(roomName => {
- document.getElementById('new-room-name').placeholder = roomName;
+ .then(res => res.json())
+ .then(data => data.randomName)
+ .then(randomName => {
+ document.getElementById('new-room-name').placeholder = randomName;
});
function escapeHTML(unsafe) {
diff --git a/client/multiplayer/room.js b/client/multiplayer/room.js
index 1b863eb54..bf04564d7 100644
--- a/client/multiplayer/room.js
+++ b/client/multiplayer/room.js
@@ -231,7 +231,7 @@ const socketOnClearStats = (message) => {
sortPlayerAccordion();
};
-const socketOnConnectionAcknowledged = (message) => {
+const socketOnConnectionAcknowledged = async (message) => {
USER_ID = message.userId;
localStorage.setItem('USER_ID', USER_ID);
@@ -243,12 +243,10 @@ const socketOnConnectionAcknowledged = (message) => {
document.getElementById('set-name').value = message.setName || '';
document.getElementById('packet-number').value = arrayToRange(message.packetNumbers) || '';
- (async () => {
- maxPacketNumber = await getNumPackets(document.getElementById('set-name').value);
- if (document.getElementById('set-name').value !== '' && maxPacketNumber === 0) {
- document.getElementById('set-name').classList.add('is-invalid');
- }
- })();
+ maxPacketNumber = await getNumPackets(document.getElementById('set-name').value);
+ if (document.getElementById('set-name').value !== '' && maxPacketNumber === 0) {
+ document.getElementById('set-name').classList.add('is-invalid');
+ }
tossup = message.tossup;
document.getElementById('set-name-info').textContent = message.tossup?.setName ?? '';
diff --git a/client/readers.js b/client/readers.js
index 2c890be9d..6663304ba 100644
--- a/client/readers.js
+++ b/client/readers.js
@@ -40,6 +40,7 @@ const SET_LIST = [];
fetch('/api/set-list')
.then(response => response.json())
+ .then(data => data.setList)
.then(data => {
document.getElementById('set-list').innerHTML = data.map(setName => `
${setName} `).join('');
data.forEach(setName => {
diff --git a/client/singleplayer/bonuses.js b/client/singleplayer/bonuses.js
index 22232442e..1cf5eb4b5 100644
--- a/client/singleplayer/bonuses.js
+++ b/client/singleplayer/bonuses.js
@@ -173,7 +173,7 @@ async function getRandomBonus(difficulties = [], categories = [], subcategories
async function giveAnswer(givenAnswer) {
- const [directive, directedPrompt] = await checkAnswer(questions[questionNumber].answers[currentBonusPart], givenAnswer);
+ const { directive, directedPrompt } = await checkAnswer(questions[questionNumber].answers[currentBonusPart], givenAnswer);
switch (directive) {
case 'accept':
@@ -479,7 +479,7 @@ document.addEventListener('keydown', (event) => {
});
-window.onload = () => {
+window.onload = async () => {
if (!sessionStorage.getItem('stats')) {
sessionStorage.setItem('stats', [0, 0, 0, 0]);
}
@@ -544,12 +544,16 @@ window.onload = () => {
if (localStorage.getItem('setNameBonusSave')) {
setName = localStorage.getItem('setNameBonusSave');
document.getElementById('set-name').value = setName;
- (async () => {
- maxPacketNumber = await getNumPackets(setName);
+ maxPacketNumber = await getNumPackets(setName);
+
+ if (setName === '') {
+ return;
+ }
+
+ if (maxPacketNumber === 0) {
+ document.getElementById('set-name').classList.add('is-invalid');
+ } else {
document.getElementById('packet-number').placeholder = `Packet Numbers (1-${maxPacketNumber})`;
- if (maxPacketNumber === 0) {
- document.getElementById('set-name').classList.add('is-invalid');
- }
- })();
+ }
}
};
diff --git a/client/singleplayer/index.js b/client/singleplayer/index.js
index da6ccf0cd..4ebd252da 100644
--- a/client/singleplayer/index.js
+++ b/client/singleplayer/index.js
@@ -10,11 +10,15 @@ let maxPacketNumber = 24;
/**
* @param {String} answerline
* @param {String} givenAnswer
- * @returns {Promise<[ "accept" | "prompt" | "reject", String | null ]>} [directive, directedPrompt]
+ * @returns {Promise<{
+ * directive: "accept" | "prompt" | "reject",
+ * directedPrompt: String | null
+ * }>}
*/
async function checkAnswer(answerline, givenAnswer) {
- if (givenAnswer === '')
- return ['reject', null];
+ if (givenAnswer === '') {
+ return { directive: 'reject', directedPrompt: null };
+ }
return await fetch(`/api/check-answer?answerline=${encodeURIComponent(answerline)}&givenAnswer=${encodeURIComponent(givenAnswer)}`)
.then(response => response.json());
@@ -91,9 +95,9 @@ document.getElementById('set-name').addEventListener('change', async function (e
this.classList.add('is-invalid');
}
maxPacketNumber = await getNumPackets(this.value);
- if (this.value === '' || maxPacketNumber > 0) {
- document.getElementById('packet-number').placeholder = `Packet Numbers (1-${maxPacketNumber})`;
- } else {
+ if (this.value === '' || maxPacketNumber === 0) {
document.getElementById('packet-number').placeholder = 'Packet Numbers';
+ } else {
+ document.getElementById('packet-number').placeholder = `Packet Numbers (1-${maxPacketNumber})`;
}
});
diff --git a/client/singleplayer/tossups.js b/client/singleplayer/tossups.js
index f62e0a685..0c7a764f4 100644
--- a/client/singleplayer/tossups.js
+++ b/client/singleplayer/tossups.js
@@ -167,7 +167,7 @@ async function getTossups(setName, packetNumber) {
async function giveAnswer(givenAnswer) {
currentlyBuzzing = false;
- const [directive, directedPrompt] = await checkAnswer(questions[questionNumber].answer, givenAnswer);
+ const { directive, directedPrompt } = await checkAnswer(questions[questionNumber].answer, givenAnswer);
switch (directive) {
case 'accept':
@@ -631,7 +631,7 @@ document.addEventListener('keydown', (event) => {
});
-window.onload = () => {
+window.onload = async () => {
for (const parameter of ['powers', 'tens', 'negs', 'dead', 'points', 'totalCelerity']) {
if (!sessionStorage.getItem(parameter))
sessionStorage.setItem(parameter, 0);
@@ -703,12 +703,16 @@ window.onload = () => {
if (localStorage.getItem('setNameTossupSave')) {
setName = localStorage.getItem('setNameTossupSave');
document.getElementById('set-name').value = setName;
- (async () => {
- maxPacketNumber = await getNumPackets(setName);
+ maxPacketNumber = await getNumPackets(setName);
+
+ if (setName === '') {
+ return;
+ }
+
+ if (maxPacketNumber === 0) {
+ document.getElementById('set-name').classList.add('is-invalid');
+ } else {
document.getElementById('packet-number').placeholder = `Packet Numbers (1-${maxPacketNumber})`;
- if (maxPacketNumber === 0) {
- document.getElementById('set-name').classList.add('is-invalid');
- }
- })();
+ }
}
};
diff --git a/client/user/stats/index.js b/client/user/stats/index.js
index 8c0e08101..463a7e44e 100644
--- a/client/user/stats/index.js
+++ b/client/user/stats/index.js
@@ -2,6 +2,7 @@ const SET_LIST = [];
fetch('/api/set-list')
.then(response => response.json())
+ .then(data => data.setList)
.then(data => {
document.getElementById('set-list').innerHTML = data.map(setName => `
${setName} `).join('');
data.forEach(setName => {
diff --git a/client/utilities.js b/client/utilities.js
index 5f4e5543c..811ee7a72 100644
--- a/client/utilities.js
+++ b/client/utilities.js
@@ -156,14 +156,13 @@ const createTossupCard = (function () {
* @returns {Promise
} The number of packets in the set.
*/
async function getNumPackets(setName) {
- if (setName === undefined) return 0;
- if (setName === '') return 0;
+ if (setName === undefined || setName === '') {
+ return 0;
+ }
return fetch(`/api/num-packets?setName=${encodeURIComponent(setName)}`)
.then(response => response.json())
- .then(data => {
- return parseInt(data);
- });
+ .then(data => data.numPackets);
}
diff --git a/constants.js b/constants.js
new file mode 100644
index 000000000..f050c6455
--- /dev/null
+++ b/constants.js
@@ -0,0 +1,52 @@
+const DEFAULT_QUERY_RETURN_LENGTH = 25;
+const MAX_QUERY_RETURN_LENGTH = 400;
+
+const DEFAULT_MIN_YEAR = 2010;
+const DEFAULT_MAX_YEAR = 2023;
+
+const DIFFICULTIES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
+const CATEGORIES = ['Literature', 'History', 'Science', 'Fine Arts', 'Religion', 'Mythology', 'Philosophy', 'Social Science', 'Current Events', 'Geography', 'Other Academic', 'Trash'];
+// const SUBCATEGORIES = [
+// ['American Literature', 'British Literature', 'Classical Literature', 'European Literature', 'World Literature', 'Other Literature'],
+// ['American History', 'Ancient History', 'European History', 'World History', 'Other History'],
+// ['Biology', 'Chemistry', 'Physics', 'Math', 'Other Science'],
+// ['Visual Fine Arts', 'Auditory Fine Arts', 'Other Fine Arts'],
+// ['Religion'],
+// ['Mythology'],
+// ['Philosophy'],
+// ['Social Science'],
+// ['Current Events'],
+// ['Geography'],
+// ['Other Academic'],
+// ['Trash']
+// ];
+const SUBCATEGORIES_FLATTENED = ['American Literature', 'British Literature', 'Classical Literature', 'European Literature', 'World Literature', 'Other Literature', 'American History', 'Ancient History', 'European History', 'World History', 'Other History', 'Biology', 'Chemistry', 'Physics', 'Math', 'Other Science', 'Visual Fine Arts', 'Auditory Fine Arts', 'Other Fine Arts', 'Religion', 'Mythology', 'Philosophy', 'Social Science', 'Current Events', 'Geography', 'Other Academic', 'Trash'];
+
+/**
+ * List of multiplayer permanent room names.
+ */
+const PERMANENT_ROOMS = ['hsquizbowl', 'collegequizbowl', 'literature', 'history', 'science', 'fine-arts'];
+
+const COOKIE_MAX_AGE = 1000 * 60 * 60 * 24 * 7; // 7 days
+
+const WEBSOCKET_MAX_PAYLOAD = 1024 * 1024 * 1; // 1 MB
+
+const ADJECTIVES = ['adaptable', 'adept', 'affectionate', 'agreeable', 'alluring', 'amazing', 'ambitious', 'amiable', 'ample', 'approachable', 'awesome', 'blithesome', 'bountiful', 'brave', 'breathtaking', 'bright', 'brilliant', 'capable', 'captivating', 'charming', 'competitive', 'confident', 'considerate', 'courageous', 'creative', 'dazzling', 'determined', 'devoted', 'diligent', 'diplomatic', 'dynamic', 'educated', 'efficient', 'elegant', 'enchanting', 'energetic', 'engaging', 'excellent', 'fabulous', 'faithful', 'fantastic', 'favorable', 'fearless', 'flexible', 'focused', 'fortuitous', 'frank', 'friendly', 'funny', 'generous', 'giving', 'gleaming', 'glimmering', 'glistening', 'glittering', 'glowing', 'gorgeous', 'gregarious', 'gripping', 'hardworking', 'helpful', 'hilarious', 'honest', 'humorous', 'imaginative', 'incredible', 'independent', 'inquisitive', 'insightful', 'kind', 'knowledgeable', 'likable', 'lovely', 'loving', 'loyal', 'lustrous', 'magnificent', 'marvelous', 'mirthful', 'moving', 'nice', 'optimistic', 'organized', 'outstanding', 'passionate', 'patient', 'perfect', 'persistent', 'personable', 'philosophical', 'plucky', 'polite', 'powerful', 'productive', 'proficient', 'propitious', 'qualified', 'ravishing', 'relaxed', 'remarkable', 'resourceful', 'responsible', 'romantic', 'rousing', 'sensible', 'shimmering', 'shining', 'sincere', 'sleek', 'sparkling', 'spectacular', 'spellbinding', 'splendid', 'stellar', 'stunning', 'stupendous', 'super', 'technological', 'thoughtful', 'twinkling', 'unique', 'upbeat', 'vibrant', 'vivacious', 'vivid', 'warmhearted', 'willing', 'wondrous', 'zestful'];
+const ANIMALS = ['aardvark', 'alligator', 'alpaca', 'anaconda', 'ant', 'anteater', 'antelope', 'aphid', 'armadillo', 'baboon', 'badger', 'barracuda', 'bat', 'beaver', 'bedbug', 'bee', 'bird', 'bison', 'bobcat', 'buffalo', 'butterfly', 'buzzard', 'camel', 'carp', 'cat', 'caterpillar', 'catfish', 'cheetah', 'chicken', 'chimpanzee', 'chipmunk', 'cobra', 'cod', 'condor', 'cougar', 'cow', 'coyote', 'crab', 'cricket', 'crocodile', 'crow', 'cuckoo', 'deer', 'dinosaur', 'dog', 'dolphin', 'donkey', 'dove', 'dragonfly', 'duck', 'eagle', 'eel', 'elephant', 'emu', 'falcon', 'ferret', 'finch', 'fish', 'flamingo', 'flea', 'fly', 'fox', 'frog', 'goat', 'goose', 'gopher', 'gorilla', 'hamster', 'hare', 'hawk', 'hippopotamus', 'horse', 'hummingbird', 'husky', 'iguana', 'impala', 'kangaroo', 'lemur', 'leopard', 'lion', 'lizard', 'llama', 'lobster', 'margay', 'monkey', 'moose', 'mosquito', 'moth', 'mouse', 'mule', 'octopus', 'orca', 'ostrich', 'otter', 'owl', 'ox', 'oyster', 'panda', 'parrot', 'peacock', 'pelican', 'penguin', 'perch', 'pheasant', 'pig', 'pigeon', 'porcupine', 'quagga', 'rabbit', 'raccoon', 'rat', 'rattlesnake', 'rooster', 'seal', 'sheep', 'skunk', 'sloth', 'snail', 'snake', 'spider', 'tiger', 'whale', 'wolf', 'wombat', 'zebra'];
+
+export {
+ DEFAULT_QUERY_RETURN_LENGTH,
+ MAX_QUERY_RETURN_LENGTH,
+ DEFAULT_MIN_YEAR,
+ DEFAULT_MAX_YEAR,
+ DIFFICULTIES,
+ CATEGORIES,
+ // SUBCATEGORIES, // not used
+ SUBCATEGORIES_FLATTENED,
+ PERMANENT_ROOMS,
+ COOKIE_MAX_AGE,
+ WEBSOCKET_MAX_PAYLOAD,
+ ADJECTIVES,
+ ANIMALS,
+};
diff --git a/database/questions.js b/database/questions.js
index 0bd196208..c4bb85269 100644
--- a/database/questions.js
+++ b/database/questions.js
@@ -1,9 +1,9 @@
-if (process.env.NODE_ENV !== 'production') {
- require('dotenv').config();
-}
+import 'dotenv/config';
+
+import { OKCYAN, ENDC, OKGREEN } from '../bcolors.js';
+import { ADJECTIVES, ANIMALS, DEFAULT_QUERY_RETURN_LENGTH, MAX_QUERY_RETURN_LENGTH, DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../constants.js';
-const { MongoClient, ObjectId } = require('mongodb');
-const { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED } = require('../server/quizbowl');
+import { MongoClient, ObjectId } from 'mongodb';
const uri = `mongodb+srv://${process.env.MONGODB_USERNAME || 'geoffreywu42'}:${process.env.MONGODB_PASSWORD || 'password'}@qbreader.0i7oej9.mongodb.net/?retryWrites=true&w=majority`;
const client = new MongoClient(uri);
@@ -11,9 +11,8 @@ client.connect().then(async () => {
console.log('connected to mongodb');
});
-const bcolors = require('../bcolors');
+
const database = client.db('qbreader');
-const quizbowl = require('../server/quizbowl');
const sets = database.collection('sets');
const tossups = database.collection('tossups');
@@ -26,12 +25,6 @@ sets.find({}, { projection: { _id: 0, name: 1 }, sort: { name: -1 } }).forEach(s
});
-const ADJECTIVES = ['adaptable', 'adept', 'affectionate', 'agreeable', 'alluring', 'amazing', 'ambitious', 'amiable', 'ample', 'approachable', 'awesome', 'blithesome', 'bountiful', 'brave', 'breathtaking', 'bright', 'brilliant', 'capable', 'captivating', 'charming', 'competitive', 'confident', 'considerate', 'courageous', 'creative', 'dazzling', 'determined', 'devoted', 'diligent', 'diplomatic', 'dynamic', 'educated', 'efficient', 'elegant', 'enchanting', 'energetic', 'engaging', 'excellent', 'fabulous', 'faithful', 'fantastic', 'favorable', 'fearless', 'flexible', 'focused', 'fortuitous', 'frank', 'friendly', 'funny', 'generous', 'giving', 'gleaming', 'glimmering', 'glistening', 'glittering', 'glowing', 'gorgeous', 'gregarious', 'gripping', 'hardworking', 'helpful', 'hilarious', 'honest', 'humorous', 'imaginative', 'incredible', 'independent', 'inquisitive', 'insightful', 'kind', 'knowledgeable', 'likable', 'lovely', 'loving', 'loyal', 'lustrous', 'magnificent', 'marvelous', 'mirthful', 'moving', 'nice', 'optimistic', 'organized', 'outstanding', 'passionate', 'patient', 'perfect', 'persistent', 'personable', 'philosophical', 'plucky', 'polite', 'powerful', 'productive', 'proficient', 'propitious', 'qualified', 'ravishing', 'relaxed', 'remarkable', 'resourceful', 'responsible', 'romantic', 'rousing', 'sensible', 'shimmering', 'shining', 'sincere', 'sleek', 'sparkling', 'spectacular', 'spellbinding', 'splendid', 'stellar', 'stunning', 'stupendous', 'super', 'technological', 'thoughtful', 'twinkling', 'unique', 'upbeat', 'vibrant', 'vivacious', 'vivid', 'warmhearted', 'willing', 'wondrous', 'zestful'];
-const ANIMALS = ['aardvark', 'alligator', 'alpaca', 'anaconda', 'ant', 'anteater', 'antelope', 'aphid', 'armadillo', 'baboon', 'badger', 'barracuda', 'bat', 'beaver', 'bedbug', 'bee', 'bird', 'bison', 'bobcat', 'buffalo', 'butterfly', 'buzzard', 'camel', 'carp', 'cat', 'caterpillar', 'catfish', 'cheetah', 'chicken', 'chimpanzee', 'chipmunk', 'cobra', 'cod', 'condor', 'cougar', 'cow', 'coyote', 'crab', 'cricket', 'crocodile', 'crow', 'cuckoo', 'deer', 'dinosaur', 'dog', 'dolphin', 'donkey', 'dove', 'dragonfly', 'duck', 'eagle', 'eel', 'elephant', 'emu', 'falcon', 'ferret', 'finch', 'fish', 'flamingo', 'flea', 'fly', 'fox', 'frog', 'goat', 'goose', 'gopher', 'gorilla', 'hamster', 'hare', 'hawk', 'hippopotamus', 'horse', 'hummingbird', 'husky', 'iguana', 'impala', 'kangaroo', 'lemur', 'leopard', 'lion', 'lizard', 'llama', 'lobster', 'margay', 'monkey', 'moose', 'mosquito', 'moth', 'mouse', 'mule', 'octopus', 'orca', 'ostrich', 'otter', 'owl', 'ox', 'oyster', 'panda', 'parrot', 'peacock', 'pelican', 'penguin', 'perch', 'pheasant', 'pig', 'pigeon', 'porcupine', 'quagga', 'rabbit', 'raccoon', 'rat', 'rattlesnake', 'rooster', 'seal', 'sheep', 'skunk', 'sloth', 'snail', 'snake', 'spider', 'tiger', 'whale', 'wolf', 'wombat', 'zebra'];
-
-const DEFAULT_QUERY_RETURN_LENGTH = 25;
-const MAX_QUERY_RETURN_LENGTH = 400;
-
/**
* Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
*/
@@ -250,15 +243,15 @@ async function getQuery({
if (verbose) {
console.log(`\
-[DATABASE] QUERY: string: ${bcolors.OKCYAN}${queryString}${bcolors.ENDC}; \
-difficulties: ${bcolors.OKGREEN}${difficulties}${bcolors.ENDC}; \
-max length: ${bcolors.OKGREEN}${maxReturnLength}${bcolors.ENDC}; \
-question type: ${bcolors.OKGREEN}${questionType}${bcolors.ENDC}; \
-ignore diacritics: ${bcolors.OKGREEN}${ignoreDiacritics}${bcolors.ENDC}; \
-randomize: ${bcolors.OKGREEN}${randomize}${bcolors.ENDC}; \
-regex: ${bcolors.OKGREEN}${regex}${bcolors.ENDC}; \
-search type: ${bcolors.OKGREEN}${searchType}${bcolors.ENDC}; \
-set name: ${bcolors.OKGREEN}${setName}${bcolors.ENDC}; \
+[DATABASE] QUERY: string: ${OKCYAN}${queryString}${ENDC}; \
+difficulties: ${OKGREEN}${difficulties}${ENDC}; \
+max length: ${OKGREEN}${maxReturnLength}${ENDC}; \
+question type: ${OKGREEN}${questionType}${ENDC}; \
+ignore diacritics: ${OKGREEN}${ignoreDiacritics}${ENDC}; \
+randomize: ${OKGREEN}${randomize}${ENDC}; \
+regex: ${OKGREEN}${regex}${ENDC}; \
+search type: ${OKGREEN}${searchType}${ENDC}; \
+set name: ${OKGREEN}${setName}${ENDC}; \
`);
console.timeEnd('getQuery');
}
@@ -376,89 +369,105 @@ function getRandomName() {
}
-async function getRandomTossups({ difficulties, categories, subcategories, number = 1, minYear = quizbowl.DEFAULT_MIN_YEAR, maxYear = quizbowl.DEFAULT_MAX_YEAR }) {
- return await getRandomQuestions({ questionType: 'tossup', difficulties, categories, subcategories, number, minYear, maxYear, verbose: false });
-}
-
-
-async function getRandomBonuses({ difficulties, categories, subcategories, number = 1, minYear = quizbowl.DEFAULT_MIN_YEAR, maxYear = quizbowl.DEFAULT_MAX_YEAR, bonusLength }) {
- if (!difficulties || difficulties.length === 0) difficulties = DIFFICULTIES;
- if (!categories || categories.length === 0) categories = CATEGORIES;
- if (!subcategories || subcategories.length === 0) subcategories = SUBCATEGORIES_FLATTENED;
-
+/**
+ * Get an array of random tossups. This method is 3-4x faster than using the randomize option in getQuery.
+ * @param {Object} object - an object containing the parameters
+ * @param {Array} object.difficulties
+ * @param {Array} object.categories
+ * @param {Array} object.subcategories
+ * @param {Number} object.number
+ * @param {Number} object.minYear
+ * @param {Number} object.maxYear
+ * @param difficulties - an array of allowed difficulty levels (1-10). Pass a 0-length array, null, or undefined to select any difficulty.
+ * @param categories - an array of allowed categories. Pass a 0-length array, null, or undefined to select any category.
+ * @param subcategories - an array of allowed subcategories. Pass a 0-length array, null, or undefined to select any subcategory.
+ * @param number - how many random tossups to return. Default: 1.
+ * @param minYear - the minimum year to select from. Default: 2010.
+ * @param maxYear - the maximum year to select from. Default: 2023.
+ * @returns {Promise>}
+ */
+async function getRandomTossups({
+ difficulties = DIFFICULTIES,
+ categories = CATEGORIES,
+ subcategories = SUBCATEGORIES_FLATTENED,
+ number = 1,
+ minYear = DEFAULT_MIN_YEAR,
+ maxYear = DEFAULT_MAX_YEAR
+} = {}) {
const aggregation = [
- { $match: {
- difficulty: { $in: difficulties },
- category: { $in: categories },
- subcategory: { $in: subcategories },
- setYear: { $gte: minYear, $lte: maxYear },
- } },
+ { $match: { setYear: { $gte: minYear, $lte: maxYear } } },
{ $sample: { size: number } },
{ $project: { reports: 0 } },
];
- if (bonusLength) {
- bonusLength = parseInt(bonusLength);
- aggregation[0].$match.parts = { $size: bonusLength };
+ if (difficulties.length) {
+ aggregation[0].$match.difficulty = { $in: difficulties };
}
- return await bonuses.aggregate(aggregation).toArray();
+ if (categories.length) {
+ aggregation[0].$match.category = { $in: categories };
+ }
+
+ if (subcategories.length) {
+ aggregation[0].$match.subcategory = { $in: subcategories };
+ }
+
+ return await tossups.aggregate(aggregation).toArray();
}
/**
- * Get an array of random questions. This method is 3-4x faster than using the randomize option in getQuery.
- * @param {'tossup' | 'bonus'} questionType - the type of question to get
- * @param {Array} difficulties - an array of allowed difficulty levels (1-10). Pass a 0-length array to select any difficulty.
- * @param {Array} categories - an array of allowed categories. Pass a 0-length array to select any category.
- * @param {Array} subcategories - an array of allowed subcategories. Pass a 0-length array to select any subcategory.
- * @param {Number} number - how many random tossups to return. Default: 1.
- * @param {Number} minYear - the minimum year to select from. Default: 2010.
- * @param {Number} maxYear - the maximum year to select from. Default: 2023.
+ * Get an array of random bonuses. This method is 3-4x faster than using the randomize option in getQuery.
+ * @param {Object} object - an object containing the parameters
+ * @param {Array} object.difficulties
+ * @param {Array} object.categories
+ * @param {Array} object.subcategories
+ * @param {Number} object.number
+ * @param {Number} object.minYear
+ * @param {Number} object.maxYear
+ * @param {Number | null | undefined} object.bonusLength
+ * @param difficulties - an array of allowed difficulty levels (1-10). Pass a 0-length array, null, or undefined to select any difficulty.
+ * @param categories - an array of allowed categories. Pass a 0-length array, null, or undefined to select any category.
+ * @param subcategories - an array of allowed subcategories. Pass a 0-length array, null, or undefined to select any subcategory.
+ * @param number - how many random bonuses to return. Default: 1.
+ * @param minYear - the minimum year to select from. Default: 2010.
+ * @param maxYear - the maximum year to select from. Default: 2023.
+ * @param bonusLength - if not null or undefined, only return bonuses with number of parts equal to `bonusLength`.
* @returns {Promise>}
*/
-async function getRandomQuestions({
- questionType = 'tossup',
- difficulties,
- categories,
- subcategories,
+async function getRandomBonuses({
+ difficulties = DIFFICULTIES,
+ categories = CATEGORIES,
+ subcategories = SUBCATEGORIES_FLATTENED,
number = 1,
- minYear = quizbowl.DEFAULT_MIN_YEAR,
- maxYear = quizbowl.DEFAULT_MAX_YEAR,
- verbose = false,
-}) {
- if (!difficulties || difficulties.length === 0) difficulties = DIFFICULTIES;
- if (!categories || categories.length === 0) categories = CATEGORIES;
- if (!subcategories || subcategories.length === 0) subcategories = SUBCATEGORIES_FLATTENED;
-
- if (verbose)
- console.log(`\
-[DATABASE] RANDOM QUESTIONS: \
-question type: ${bcolors.OKGREEN}${questionType}${bcolors.ENDC}; \
-difficulties: ${bcolors.OKGREEN}${difficulties}${bcolors.ENDC}; \
-years: ${bcolors.OKGREEN}${minYear} to ${maxYear}${bcolors.ENDC}; \
-number: ${bcolors.OKGREEN}${number}${bcolors.ENDC}; \
-categories: ${bcolors.OKCYAN}${categories}${bcolors.ENDC}; \
-subcategories: ${bcolors.OKCYAN}${subcategories}${bcolors.ENDC};\
-`);
-
+ minYear = DEFAULT_MIN_YEAR,
+ maxYear = DEFAULT_MAX_YEAR,
+ bonusLength
+} = {}) {
const aggregation = [
- { $match: {
- difficulty: { $in: difficulties },
- category: { $in: categories },
- subcategory: { $in: subcategories },
- setYear: { $gte: minYear, $lte: maxYear },
- } },
+ { $match: { setYear: { $gte: minYear, $lte: maxYear } } },
{ $sample: { size: number } },
{ $project: { reports: 0 } },
];
- switch (questionType) {
- case 'tossup':
- return await tossups.aggregate(aggregation).toArray();
- case 'bonus':
- return await bonuses.aggregate(aggregation).toArray();
+ if (difficulties.length) {
+ aggregation[0].$match.difficulty = { $in: difficulties };
+ }
+
+ if (categories.length) {
+ aggregation[0].$match.category = { $in: categories };
+ }
+
+ if (subcategories.length) {
+ aggregation[0].$match.subcategory = { $in: subcategories };
+ }
+
+ if (bonusLength) {
+ bonusLength = parseInt(bonusLength);
+ aggregation[0].$match.parts = { $size: bonusLength };
}
+
+ return await bonuses.aggregate(aggregation).toArray();
}
@@ -576,15 +585,13 @@ async function reportQuestion(_id, reason, description, verbose = true) {
}
-module.exports = {
- DEFAULT_QUERY_RETURN_LENGTH,
+export {
getNumPackets,
getPacket,
getQuery,
getRandomName,
getRandomTossups,
getRandomBonuses,
- getRandomQuestions,
getSet,
getSetId,
getSetList,
diff --git a/database/schemas.js b/database/schemas.js
index b51c51845..88bcdbf77 100644
--- a/database/schemas.js
+++ b/database/schemas.js
@@ -1,6 +1,6 @@
// database schemas
-const { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED_ALL } = require('../server/quizbowl');
+import { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED_ALL } from '../constants';
const schemas = {
tossup: {
@@ -225,4 +225,4 @@ const schemas = {
}
};
-module.exports = schemas;
+export default schemas;
diff --git a/database/users.js b/database/users.js
index d5eb61a07..3364cb30d 100644
--- a/database/users.js
+++ b/database/users.js
@@ -1,4 +1,6 @@
-const { MongoClient, ObjectId } = require('mongodb');
+import { getSetId, getTossupById } from './questions.js';
+
+import { MongoClient, ObjectId } from 'mongodb';
const uri = `mongodb+srv://${process.env.MONGODB_USERNAME || 'geoffreywu42'}:${process.env.MONGODB_PASSWORD || 'password'}@qbreader.0i7oej9.mongodb.net/?retryWrites=true&w=majority`;
const client = new MongoClient(uri);
@@ -10,7 +12,6 @@ const database = client.db('account-info');
const users = database.collection('users');
const tossupData = database.collection('tossup-data');
const bonusData = database.collection('bonus-data');
-const { getSetId, getTossupById } = require('./questions');
const username_to_id = {};
@@ -403,7 +404,7 @@ async function verifyEmail(user_id) {
);
}
-module.exports = {
+export {
createUser,
getBestBuzz,
getCategoryStats,
diff --git a/package-lock.json b/package-lock.json
index 9d8a841ac..b4796269f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "qbreader",
- "version": "3.3.6",
+ "version": "4.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "qbreader",
- "version": "3.3.6",
+ "version": "4.0.0",
"dependencies": {
"bootstrap": "5.2.3",
"cookie-session": "^2.0.0",
@@ -24,6 +24,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roman-numerals": "^0.3.2",
+ "stemmer": "^2.0.1",
"uuid": "^8.3.2",
"ws": "^8.8.0"
},
@@ -4122,6 +4123,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/stemmer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/stemmer/-/stemmer-2.0.1.tgz",
+ "integrity": "sha512-bkWvSX2JR4nSZFfs113kd4C6X13bBBrg4fBKv2pVdzpdQI2LA5pZcWzTFNdkYsiUNl13E4EzymSRjZ0D55jBYg==",
+ "bin": {
+ "stemmer": "cli.js"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
@@ -7552,6 +7565,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
+ "stemmer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/stemmer/-/stemmer-2.0.1.tgz",
+ "integrity": "sha512-bkWvSX2JR4nSZFfs113kd4C6X13bBBrg4fBKv2pVdzpdQI2LA5pZcWzTFNdkYsiUNl13E4EzymSRjZ0D55jBYg=="
+ },
"string.prototype.matchall": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
diff --git a/package.json b/package.json
index a32333390..07f7054d3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "qbreader",
- "version": "3.3.6",
+ "version": "4.0.0",
+ "type": "module",
"scripts": {
"build": "npm run compile-react && npm run sass",
"compile-react": "babel client/database/index.jsx -o client/database/index.js",
@@ -27,6 +28,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roman-numerals": "^0.3.2",
+ "stemmer": "^2.0.1",
"uuid": "^8.3.2",
"ws": "^8.8.0"
},
diff --git a/routes/about.js b/routes/about.js
index c6262a9b2..2811f9c91 100644
--- a/routes/about.js
+++ b/routes/about.js
@@ -1,8 +1,8 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (req, res) => {
res.sendFile('about.html', { root: './client' });
});
-module.exports = router;
+export default router;
diff --git a/routes/api-docs.js b/routes/api-docs.js
index 84f141b69..ced78a87f 100644
--- a/routes/api-docs.js
+++ b/routes/api-docs.js
@@ -1,6 +1,7 @@
-const express = require('express');
-const fs = require('fs');
-const router = express.Router();
+import { Router } from 'express';
+import { readdirSync } from 'fs';
+
+const router = Router();
const docsDir = './client/api-docs';
router.get('/', (req, res) => {
@@ -8,7 +9,7 @@ router.get('/', (req, res) => {
});
// routes every html doc in /client/api-docs
-fs.readdirSync(docsDir).forEach(file => {
+readdirSync(docsDir).forEach(file => {
if (file.endsWith('.html') && file !== 'index.html') {
const route = file.substring(0, file.length - 5);
router.get(`/${route}`, (req, res) => {
@@ -17,7 +18,7 @@ fs.readdirSync(docsDir).forEach(file => {
}
});
-fs.readdirSync(`${docsDir}/multiplayer`).forEach(file => {
+readdirSync(`${docsDir}/multiplayer`).forEach(file => {
if (file.endsWith('.html') && file !== 'index.html') {
const route = file.substring(0, file.length - 5);
router.get(`/multiplayer/${route}`, (req, res) => {
@@ -25,4 +26,5 @@ fs.readdirSync(`${docsDir}/multiplayer`).forEach(file => {
});
}
});
-module.exports = router;
+
+export default router;
diff --git a/routes/api.js b/routes/api.js
index 4970a6c26..6067689f2 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -1,20 +1,20 @@
-const express = require('express');
-const router = express.Router();
+import { DEFAULT_QUERY_RETURN_LENGTH } from '../constants.js';
+import { getNumPackets, getPacket, getQuery, getRandomName, getRandomBonuses, getRandomTossups, reportQuestion, getSetList } from '../database/questions.js';
+import checkAnswer from '../server/checkAnswer.js';
+import { tossupRooms } from '../server/TossupRoom.js';
-const database = require('../database/questions');
-const { checkAnswer } = require('../server/scorer');
+import { Router } from 'express';
+import rateLimit from 'express-rate-limit';
-const rateLimit = require('express-rate-limit');
-const apiLimiter = rateLimit({
+const router = Router();
+// Apply the rate limiting middleware to API calls only
+router.use(rateLimit({
windowMs: 1000, // 4 seconds
max: 20, // Limit each IP to 20 requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
-});
-
-// Apply the rate limiting middleware to API calls only
-router.use(apiLimiter);
+}));
// express encodes same parameter passed multiple times as an array
@@ -31,24 +31,24 @@ router.use((req, _res, next) => {
router.get('/check-answer', (req, res) => {
const { answerline, givenAnswer } = req.query;
- const [directive, directedPrompt] = checkAnswer(answerline, givenAnswer);
- res.send(JSON.stringify([directive, directedPrompt]));
+ const { directive, directedPrompt } = checkAnswer(answerline, givenAnswer);
+ res.send(JSON.stringify({ directive: directive, directedPrompt: directedPrompt }));
});
router.get('/num-packets', async (req, res) => {
- const numPackets = await database.getNumPackets(req.query.setName);
+ const numPackets = await getNumPackets(req.query.setName);
if (numPackets === 0) {
res.statusCode = 404;
}
- res.send(numPackets.toString());
+ res.send(JSON.stringify({ numPackets: numPackets }));
});
router.get('/packet', async (req, res) => {
const setName = req.query.setName;
const packetNumber = parseInt(req.query.packetNumber);
- const packet = await database.getPacket({ setName, packetNumber });
+ const packet = await getPacket({ setName, packetNumber });
if (packet.tossups.length === 0 && packet.bonuses.length === 0) {
res.statusCode = 404;
}
@@ -59,7 +59,7 @@ router.get('/packet', async (req, res) => {
router.get('/packet-bonuses', async (req, res) => {
const setName = req.query.setName;
const packetNumber = parseInt(req.query.packetNumber);
- const packet = await database.getPacket({ setName, packetNumber, questionTypes: ['bonuses'] });
+ const packet = await getPacket({ setName, packetNumber, questionTypes: ['bonuses'] });
if (packet.bonuses.length === 0) {
res.statusCode = 404;
}
@@ -70,7 +70,7 @@ router.get('/packet-bonuses', async (req, res) => {
router.get('/packet-tossups', async (req, res) => {
const setName = req.query.setName;
const packetNumber = parseInt(req.query.packetNumber);
- const packet = await database.getPacket({ setName, packetNumber, questionTypes: ['tossups'] });
+ const packet = await getPacket({ setName, packetNumber, questionTypes: ['tossups'] });
if (packet.tossups.length === 0) {
res.statusCode = 404;
}
@@ -121,7 +121,7 @@ router.get('/query', async (req, res) => {
}
if (!req.query.maxReturnLength || isNaN(req.query.maxReturnLength)) {
- req.query.maxReturnLength = database.DEFAULT_QUERY_RETURN_LENGTH;
+ req.query.maxReturnLength = DEFAULT_QUERY_RETURN_LENGTH;
}
const maxPagination = Math.floor(4000 / (req.query.maxReturnLength || 25));
@@ -135,27 +135,35 @@ router.get('/query', async (req, res) => {
req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear);
req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear);
- const queryResult = await database.getQuery(req.query);
+ const queryResult = await getQuery(req.query);
res.send(JSON.stringify(queryResult));
});
router.get('/random-name', (req, res) => {
- res.send(database.getRandomName());
+ const randomName = getRandomName();
+ res.send(JSON.stringify({ randomName: randomName }));
});
router.get('/random-bonus', async (req, res) => {
- if (req.query.difficulties)
+ if (req.query.difficulties) {
req.query.difficulties = req.query.difficulties
.split(',')
.map((difficulty) => parseInt(difficulty));
- if (req.query.categories)
+ req.query.difficulties = req.query.difficulties.length ? req.query.difficulties : undefined;
+ }
+
+ if (req.query.categories) {
req.query.categories = req.query.categories.split(',');
+ req.query.categories = req.query.categories.length ? req.query.categories : undefined;
+ }
- if (req.query.subcategories)
+ if (req.query.subcategories) {
req.query.subcategories = req.query.subcategories.split(',');
+ req.query.subcategories = req.query.subcategories.length ? req.query.subcategories : undefined;
+ }
req.query.bonusLength = (req.query.threePartBonuses === 'true') ? 3 : undefined;
@@ -163,7 +171,7 @@ router.get('/random-bonus', async (req, res) => {
req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear);
req.query.number = isNaN(req.query.number) ? undefined : parseInt(req.query.number);
- const bonuses = await database.getRandomBonuses(req.query);
+ const bonuses = await getRandomBonuses(req.query);
if (bonuses.length === 0) {
res.status(404);
}
@@ -172,22 +180,29 @@ router.get('/random-bonus', async (req, res) => {
router.get('/random-tossup', async (req, res) => {
- if (req.query.difficulties)
+ if (req.query.difficulties) {
req.query.difficulties = req.query.difficulties
.split(',')
.map((difficulty) => parseInt(difficulty));
- if (req.query.categories)
+ req.query.difficulties = req.query.difficulties.length ? req.query.difficulties : undefined;
+ }
+
+ if (req.query.categories) {
req.query.categories = req.query.categories.split(',');
+ req.query.categories = req.query.categories.length ? req.query.categories : undefined;
+ }
- if (req.query.subcategories)
+ if (req.query.subcategories) {
req.query.subcategories = req.query.subcategories.split(',');
+ req.query.subcategories = req.query.subcategories.length ? req.query.subcategories : undefined;
+ }
req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear);
req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear);
req.query.number = isNaN(req.query.number) ? undefined : parseInt(req.query.number);
- const tossups = await database.getRandomTossups(req.query);
+ const tossups = await getRandomTossups(req.query);
if (tossups.length === 0) {
res.status(404);
}
@@ -196,47 +211,11 @@ router.get('/random-tossup', async (req, res) => {
});
-// DEPRECATED and will be removed in the future
-router.post('/random-question', async (req, res) => {
- if (!['tossup', 'bonus'].includes(req.body.questionType)) {
- res.status(400).send('Invalid question type specified.');
- return;
- }
-
- if (typeof req.body.difficulties === 'string') {
- req.body.difficulties = parseInt(req.body.difficulties);
- }
-
- if (typeof req.body.difficulties === 'number') {
- req.body.difficulties = [req.body.difficulties];
- }
-
- if (typeof req.body.categories === 'string') {
- req.body.categories = [req.body.categories];
- }
-
- if (typeof req.body.subcategories === 'string') {
- req.body.subcategories = [req.body.subcategories];
- }
-
- req.body.minYear = isNaN(req.body.minYear) ? undefined : parseInt(req.body.minYear);
- req.body.maxYear = isNaN(req.body.maxYear) ? undefined : parseInt(req.body.maxYear);
- req.body.number = isNaN(req.body.number) ? undefined : parseInt(req.body.number);
-
- const questions = await database.getRandomQuestions(req.body);
- if (questions.length > 0) {
- res.send(JSON.stringify(questions));
- } else {
- res.sendStatus(404);
- }
-});
-
-
router.post('/report-question', async (req, res) => {
const _id = req.body._id;
const reason = req.body.reason ?? '';
const description = req.body.description ?? '';
- const successful = await database.reportQuestion(_id, reason, description);
+ const successful = await reportQuestion(_id, reason, description);
if (successful) {
res.sendStatus(200);
} else {
@@ -246,9 +225,28 @@ router.post('/report-question', async (req, res) => {
router.get('/set-list', (req, res) => {
- const setList = database.getSetList(req.query.setName);
- res.send(setList);
+ const setList = getSetList();
+ res.send(JSON.stringify({ setList }));
+});
+
+
+router.get('/multiplayer/room-list', (_req, res) => {
+ const roomList = [];
+ for (const roomName in tossupRooms) {
+ if (!tossupRooms[roomName].settings.public) {
+ continue;
+ }
+
+ roomList.push({
+ roomName: roomName,
+ playerCount: Object.keys(tossupRooms[roomName].players).length,
+ onlineCount: Object.keys(tossupRooms[roomName].sockets).length,
+ isPermanent: tossupRooms[roomName].isPermanent,
+ });
+ }
+
+ res.send(JSON.stringify({ roomList: roomList }));
});
-module.exports = router;
+export default router;
diff --git a/routes/auth.js b/routes/auth.js
index 6aa85f538..4ad6cd7dc 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -1,20 +1,19 @@
-const express = require('express');
-const router = express.Router();
+import { COOKIE_MAX_AGE } from '../constants.js';
+import * as userDB from '../database/users.js';
+import { checkPassword, checkToken, generateToken, saltAndHashPassword, sendVerificationEmail, updatePassword, verifyEmailLink, sendResetPasswordEmail, verifyResetPasswordLink } from '../server/authentication.js';
-const { checkPassword, checkToken, generateToken, saltAndHashPassword, sendVerificationEmail, updatePassword, verifyEmailLink, sendResetPasswordEmail, verifyResetPasswordLink } = require('../server/authentication');
-const { ObjectId } = require('mongodb');
-const userDB = require('../database/users');
+import { Router } from 'express';
+import rateLimit from 'express-rate-limit';
+import { ObjectId } from 'mongodb';
-const rateLimit = require('express-rate-limit');
-const apiLimiter = rateLimit({
+const router = Router();
+router.use(rateLimit({
windowMs: 1000, // 4 seconds
max: 20, // Limit each IP to 20 requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
-});
-router.use(apiLimiter);
+}));
-const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
router.post('/edit-profile', async (req, res) => {
const { username, token } = req.session;
@@ -50,7 +49,7 @@ router.post('/edit-password', async (req, res) => {
await updatePassword(username, req.body.newPassword);
- const expires = Date.now() + maxAge;
+ const expires = Date.now() + COOKIE_MAX_AGE;
const verifiedEmail = await userDB.getUserField(username, 'verifiedEmail');
req.session.username = username;
req.session.token = generateToken(username, verifiedEmail);
@@ -88,7 +87,7 @@ router.post('/login', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
if (await checkPassword(username, password)) {
- const expires = Date.now() + maxAge;
+ const expires = Date.now() + COOKIE_MAX_AGE;
const verifiedEmail = await userDB.getUserField(username, 'verifiedEmail');
req.session.username = username;
req.session.token = generateToken(username, verifiedEmail);
@@ -197,7 +196,7 @@ router.post('/signup', async (req, res) => {
res.sendStatus(409);
} else {
// log the user in when they sign up
- const expires = Date.now() + maxAge;
+ const expires = Date.now() + COOKIE_MAX_AGE;
req.session.username = username;
req.session.token = generateToken(username);
req.session.expires = expires;
@@ -352,4 +351,4 @@ router.get('/user-stats/tossup', async (req, res) => {
});
-module.exports = router;
+export default router;
diff --git a/routes/backups.js b/routes/backups.js
index a677c2100..df1f35ce1 100644
--- a/routes/backups.js
+++ b/routes/backups.js
@@ -1,8 +1,8 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (_req, res) => {
res.sendFile('backups.html', { root: './client' });
});
-module.exports = router;
+export default router;
diff --git a/routes/bonuses.js b/routes/bonuses.js
index d99ca1121..d090061e7 100644
--- a/routes/bonuses.js
+++ b/routes/bonuses.js
@@ -1,8 +1,8 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (req, res) => {
res.sendFile('bonuses.html', { root: './client/singleplayer' });
});
-module.exports = router;
+export default router;
diff --git a/routes/database.js b/routes/database.js
index d601f643b..bbfec6b7c 100644
--- a/routes/database.js
+++ b/routes/database.js
@@ -1,8 +1,8 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (req, res) => {
res.sendFile('index.html', { root: './client/database' });
});
-module.exports = router;
+export default router;
diff --git a/routes/index.js b/routes/index.js
index 50fa03b9f..162e20e82 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,5 +1,5 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (req, res) => {
res.sendFile('index.html', { root: './client' });
@@ -9,4 +9,4 @@ router.get('/api-info', (req, res) => {
res.redirect('/api-docs');
});
-module.exports = router;
+export default router;
diff --git a/routes/multiplayer.js b/routes/multiplayer.js
index baf5a6e73..76a334d63 100644
--- a/routes/multiplayer.js
+++ b/routes/multiplayer.js
@@ -1,5 +1,5 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (req, res) => {
res.sendFile('index.html', { root: './client/multiplayer' });
@@ -9,4 +9,4 @@ router.get('/*', (req, res) => {
res.sendFile('room.html', { root: './client/multiplayer' });
});
-module.exports = router;
+export default router;
diff --git a/routes/tossups.js b/routes/tossups.js
index 964f9ab7f..1410bef34 100644
--- a/routes/tossups.js
+++ b/routes/tossups.js
@@ -1,8 +1,8 @@
-const express = require('express');
-const router = express.Router();
+import { Router } from 'express';
+const router = Router();
router.get('/', (req, res) => {
res.sendFile('tossups.html', { root: './client/singleplayer' });
});
-module.exports = router;
+export default router;
diff --git a/routes/user.js b/routes/user.js
index 9fea1854c..9d3c60fc3 100644
--- a/routes/user.js
+++ b/routes/user.js
@@ -1,13 +1,14 @@
-const express = require('express');
-const router = express.Router();
+import { checkToken } from '../server/authentication.js';
-const authentication = require('../server/authentication');
+import { Router } from 'express';
+const router = Router();
+
function getPageSecurely(htmlFile) {
return async (req, res) => {
// don't show page if you're not logged in
- if (!req.session || !authentication.checkToken(req.session.username, req.session.token)) {
+ if (!req.session || !checkToken(req.session.username, req.session.token)) {
res.redirect('/user/login');
return;
}
@@ -25,7 +26,7 @@ router.get('/forgot-password', async (req, res) => {
router.get('/login', async (req, res) => {
// don't show login page if you're already logged in
- if (req.session && authentication.checkToken(req.session.username, req.session.token)) {
+ if (req.session && checkToken(req.session.username, req.session.token)) {
res.redirect('/user/my-profile');
return;
}
@@ -53,4 +54,4 @@ router.get('/verify-failed', async (req, res) => {
});
-module.exports = router;
+export default router;
diff --git a/server/Player.js b/server/Player.js
index 4d1766053..023a882e3 100644
--- a/server/Player.js
+++ b/server/Player.js
@@ -65,4 +65,4 @@ class Player {
}
}
-module.exports = Player;
+export default Player;
diff --git a/server/TossupRoom.js b/server/TossupRoom.js
index 521c24463..c352c7a7d 100644
--- a/server/TossupRoom.js
+++ b/server/TossupRoom.js
@@ -1,14 +1,24 @@
-const bcolors = require('../bcolors');
-const database = require('../database/questions');
-const Player = require('./Player');
-const scorer = require('./scorer');
-const quizbowl = require('./quizbowl');
-
-const createDOMPurify = require('dompurify');
-const { JSDOM } = require('jsdom');
+import checkAnswer from './checkAnswer.js';
+import Player from './Player.js';
+import { HEADER, ENDC, OKBLUE, OKGREEN } from '../bcolors.js';
+import { getSet, getRandomTossups } from '../database/questions.js';
+import { DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR, PERMANENT_ROOMS } from '../constants.js';
+
+import createDOMPurify from 'dompurify';
+import { JSDOM } from 'jsdom';
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
+
+/**
+ * @returns {Number} The number of points scored on a tossup.
+ */
+function scoreTossup({ isCorrect, inPower, endOfQuestion, isPace = false }) {
+ const powerValue = isPace ? 20 : 15;
+ const negValue = isPace ? 0 : -5;
+ return isCorrect ? (inPower ? powerValue : 10) : (endOfQuestion ? 0 : negValue);
+}
+
const RateLimit = require('./RateLimit');
const rateLimit1 = new RateLimit(50, 1000);
@@ -36,8 +46,8 @@ class TossupRoom {
this.query = {
difficulties: [4, 5],
- minYear: quizbowl.DEFAULT_MIN_YEAR,
- maxYear: quizbowl.DEFAULT_MAX_YEAR,
+ minYear: DEFAULT_MIN_YEAR,
+ maxYear: DEFAULT_MAX_YEAR,
packetNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24],
setName: '2022 PACE NSC',
categories: [],
@@ -57,10 +67,10 @@ class TossupRoom {
}
connection(socket, userId, username) {
- console.log(`Connection in room ${bcolors.HEADER}${this.name}${bcolors.ENDC} - userId: ${bcolors.OKBLUE}${userId}${bcolors.ENDC}, username: ${bcolors.OKBLUE}${username}${bcolors.ENDC} - with settings ${bcolors.OKGREEN}${Object.keys(this.settings).map(key => [key, this.settings[key]].join(': ')).join('; ')};${bcolors.ENDC}`);
+ console.log(`Connection in room ${HEADER}${this.name}${ENDC} - userId: ${OKBLUE}${userId}${ENDC}, username: ${OKBLUE}${username}${ENDC} - with settings ${OKGREEN}${Object.keys(this.settings).map(key => [key, this.settings[key]].join(': ')).join('; ')};${ENDC}`);
socket.on('message', message => {
if (rateLimit1(socket) && !this.rateLimitExceeded.has(username)) {
- console.log(`Rate limit exceeded for ${bcolors.OKBLUE}${username}${bcolors.ENDC} in room ${bcolors.HEADER}${this.name}${bcolors.ENDC}`);
+ console.log(`Rate limit exceeded for ${OKBLUE}${username}${ENDC} in room ${HEADER}${this.name}${ENDC}`);
this.rateLimitExceeded.add(username);
return;
}
@@ -312,8 +322,8 @@ class TossupRoom {
break;
case 'year-range': {
- const minYear = isNaN(message.minYear) ? quizbowl.DEFAULT_MIN_YEAR : parseInt(message.minYear);
- const maxYear = isNaN(message.maxYear) ? quizbowl.DEFAULT_MAX_YEAR : parseInt(message.maxYear);
+ const minYear = isNaN(message.minYear) ? DEFAULT_MIN_YEAR : parseInt(message.minYear);
+ const maxYear = isNaN(message.maxYear) ? DEFAULT_MAX_YEAR : parseInt(message.maxYear);
this.sendSocketMessage({
type: 'year-range',
minYear: minYear,
@@ -339,11 +349,11 @@ class TossupRoom {
if (this.settings.selectBySetName) {
this.questionNumber = 0;
- database.getSet(this.query).then(set => {
+ getSet(this.query).then(set => {
this.setCache = set;
});
} else {
- database.getRandomTossups(this.query).then(tossups => {
+ getRandomTossups(this.query).then(tossups => {
this.randomQuestionCache = tossups;
});
}
@@ -369,7 +379,7 @@ class TossupRoom {
}
} else {
if (this.randomQuestionCache.length === 0) {
- this.randomQuestionCache = await database.getRandomTossups(this.query);
+ this.randomQuestionCache = await getRandomTossups(this.query);
if (this.randomQuestionCache.length === 0) {
this.tossup = {};
this.sendSocketMessage({
@@ -440,8 +450,8 @@ class TossupRoom {
const celerity = this.questionSplit.slice(this.wordIndex).join(' ').length / this.tossup.question.length;
const endOfQuestion = (this.wordIndex === this.questionSplit.length);
const inPower = this.questionSplit.indexOf('(*)') >= this.wordIndex;
- const [directive, directedPrompt] = scorer.checkAnswer(this.tossup.answer, givenAnswer);
- const points = quizbowl.scoreTossup({
+ const { directive, directedPrompt } = checkAnswer(this.tossup.answer, givenAnswer);
+ const points = scoreTossup({
isCorrect: directive === 'accept',
inPower,
endOfQuestion,
@@ -585,4 +595,28 @@ class TossupRoom {
}
}
-module.exports = TossupRoom;
+const tossupRooms = {};
+
+for (const roomName of PERMANENT_ROOMS) {
+ tossupRooms[roomName] = new TossupRoom(roomName, true);
+}
+
+
+/**
+ * Returns the room with the given room name.
+ * If the room does not exist, it is created.
+ * @param {String} roomName
+ * @returns {TossupRoom}
+ */
+function createAndReturnRoom(roomName) {
+ roomName = DOMPurify.sanitize(roomName);
+
+ if (!Object.prototype.hasOwnProperty.call(tossupRooms, roomName)) {
+ tossupRooms[roomName] = new TossupRoom(roomName, false);
+ }
+
+ return tossupRooms[roomName];
+}
+
+
+export { createAndReturnRoom, tossupRooms };
diff --git a/server/authentication.js b/server/authentication.js
index 8ecb216f0..5fceb9643 100644
--- a/server/authentication.js
+++ b/server/authentication.js
@@ -1,15 +1,16 @@
-const crypto = require('crypto');
-const jwt = require('jsonwebtoken');
-const users = require('../database/users');
+import 'dotenv/config';
-const baseURL = process.env.BASE_URL ?? (process.env.NODE_ENV === 'production' ? 'https://www.qbreader.org' : 'http://localhost:3000');
+import { getUserField, getUserId, updateUser, verifyEmail } from '../database/users.js';
-if (process.env.NODE_ENV !== 'production') {
- require('dotenv').config();
-}
+import { createHash } from 'crypto';
+import jsonwebtoken from 'jsonwebtoken';
+const { sign, verify } = jsonwebtoken;
+import { createTransport } from 'nodemailer';
+
+
+const baseURL = process.env.BASE_URL ?? (process.env.NODE_ENV === 'production' ? 'https://www.qbreader.org' : 'http://localhost:3000');
-const nodemailer = require('nodemailer');
-const transporter = nodemailer.createTransport({
+const transporter = createTransport({
host: 'smtp.sendgrid.net',
port: 465,
secure: true,
@@ -48,7 +49,7 @@ const activeResetPasswordTokens = {};
* @returns {Promise}
*/
async function checkPassword(username, password) {
- return await users.getUserField(username, 'password') === saltAndHashPassword(password);
+ return await getUserField(username, 'password') === saltAndHashPassword(password);
}
@@ -60,7 +61,7 @@ async function checkPassword(username, password) {
* @returns {Boolean} True if the token is valid, and false otherwise.
*/
function checkToken(username, token, checkEmailVerification = false) {
- return jwt.verify(token, secret, (err, decoded) => {
+ return verify(token, secret, (err, decoded) => {
if (err) {
return false;
} else {
@@ -77,7 +78,7 @@ function checkToken(username, token, checkEmailVerification = false) {
* @returns A JWT token.
*/
function generateToken(username, verifiedEmail = false) {
- return jwt.sign({ username, verifiedEmail }, secret);
+ return sign({ username, verifiedEmail }, secret);
}
@@ -88,22 +89,22 @@ function generateToken(username, verifiedEmail = false) {
*/
function saltAndHashPassword(password) {
password = salt + password + salt;
- const hash = crypto.createHash('sha256').update(password).digest('base64');
- const hash2 = crypto.createHash('sha256').update(hash).digest('base64');
- const hash3 = crypto.createHash('sha256').update(hash2).digest('base64');
+ const hash = createHash('sha256').update(password).digest('base64');
+ const hash2 = createHash('sha256').update(hash).digest('base64');
+ const hash3 = createHash('sha256').update(hash2).digest('base64');
return hash3;
}
async function sendResetPasswordEmail(username) {
- const email = await users.getUserField(username, 'email');
- const user_id = await users.getUserId(username);
+ const email = await getUserField(username, 'email');
+ const user_id = await getUserId(username);
if (!user_id || !email) {
return false;
}
const timestamp = Date.parse((new Date()).toString());
- const token = jwt.sign({ user_id, timestamp }, secret);
+ const token = sign({ user_id, timestamp }, secret);
const url = `${baseURL}/auth/verify-reset-password?user_id=${user_id}&token=${token}`;
const message = {
from: 'info@qbreader.org',
@@ -125,14 +126,14 @@ async function sendResetPasswordEmail(username) {
async function sendVerificationEmail(username) {
- const email = await users.getUserField(username, 'email');
- const user_id = await users.getUserId(username);
+ const email = await getUserField(username, 'email');
+ const user_id = await getUserId(username);
if (!user_id || !email) {
return false;
}
const timestamp = Date.parse((new Date()).toString());
- const token = jwt.sign({ user_id, timestamp }, secret);
+ const token = sign({ user_id, timestamp }, secret);
const url = `${baseURL}/auth/verify-email?user_id=${user_id}&token=${token}`;
const message = {
from: 'info@qbreader.org',
@@ -154,13 +155,13 @@ async function sendVerificationEmail(username) {
function updatePassword(username, newPassword) {
- return users.updateUser(username, { password: saltAndHashPassword(newPassword) });
+ return updateUser(username, { password: saltAndHashPassword(newPassword) });
}
function verifyEmailLink(user_id, token) {
const expirationTime = 1000 * 60 * 15; // 15 minutes
- return jwt.verify(token, secret, (err, decoded) => {
+ return verify(token, secret, (err, decoded) => {
if (err) {
return false;
}
@@ -184,7 +185,7 @@ function verifyEmailLink(user_id, token) {
return false;
}
- users.verifyEmail(user_id);
+ verifyEmail(user_id);
return true;
});
}
@@ -192,7 +193,7 @@ function verifyEmailLink(user_id, token) {
function verifyResetPasswordLink(user_id, token) {
const expirationTime = 1000 * 60 * 15; // 15 minutes
- return jwt.verify(token, secret, (err, decoded) => {
+ return verify(token, secret, (err, decoded) => {
if (err) {
return false;
}
@@ -221,7 +222,7 @@ function verifyResetPasswordLink(user_id, token) {
}
-module.exports = {
+export {
checkPassword,
checkToken,
generateToken,
diff --git a/server/scorer.js b/server/checkAnswer.js
similarity index 72%
rename from server/scorer.js
rename to server/checkAnswer.js
index 52fd7a22c..cc84ad605 100644
--- a/server/scorer.js
+++ b/server/checkAnswer.js
@@ -1,188 +1,13 @@
-const { distance } = require('damerau-levenshtein-js');
-const { toWords } = require('number-to-words');
-const { toArabic } = require('roman-numerals');
+import { distance } from 'damerau-levenshtein-js';
+import numberToWords from 'number-to-words';
+import { toArabic } from 'roman-numerals';
+import { stemmer } from 'stemmer';
+
+const { toWords } = numberToWords;
// const METAWORDS = ['the', 'like', 'descriptions', 'description', 'of', 'do', 'not', 'as', 'accept', 'or', 'other', 'prompt', 'on', 'except', 'before', 'after', 'is', 'read', 'stated', 'mentioned', 'at', 'any', 'don\'t', 'more', 'specific', 'etc', 'eg', 'answers', 'word', 'forms'];
const METAWORDS = [];
-/**
- * Implements the Porter Stemming Algorithm.
- * Source: https://tartarus.org/martin/PorterStemmer/js.txt
- */
-const stemmer = (() => {
- const step2list = {
- 'ational' : 'ate',
- 'tional' : 'tion',
- 'enci' : 'ence',
- 'anci' : 'ance',
- 'izer' : 'ize',
- 'bli' : 'ble',
- 'alli' : 'al',
- 'entli' : 'ent',
- 'eli' : 'e',
- 'ousli' : 'ous',
- 'ization' : 'ize',
- 'ation' : 'ate',
- 'ator' : 'ate',
- 'alism' : 'al',
- 'iveness' : 'ive',
- 'fulness' : 'ful',
- 'ousness' : 'ous',
- 'aliti' : 'al',
- 'iviti' : 'ive',
- 'biliti' : 'ble',
- 'logi' : 'log'
- },
-
- step3list = {
- 'icate' : 'ic',
- 'ative' : '',
- 'alize' : 'al',
- 'iciti' : 'ic',
- 'ical' : 'ic',
- 'ful' : '',
- 'ness' : ''
- },
-
- c = '[^aeiou]', // consonant
- v = '[aeiouy]', // vowel
- C = c + '[^aeiouy]*', // consonant sequence
- V = v + '[aeiou]*', // vowel sequence
-
- mgr0 = '^(' + C + ')?' + V + C, // [C]VC... is m>0
- meq1 = '^(' + C + ')?' + V + C + '(' + V + ')?$', // [C]VC[V] is m=1
- mgr1 = '^(' + C + ')?' + V + C + V + C, // [C]VCVC... is m>1
- s_v = '^(' + C + ')?' + v; // vowel in stem
-
- return function (w) {
- let stem,
- suffix,
- re,
- re2,
- re3,
- re4;
-
- if (w.length < 3) { return w; }
-
- const firstch = w.substr(0,1);
- if (firstch == 'y') {
- w = firstch.toUpperCase() + w.substr(1);
- }
-
- // Step 1a
- re = /^(.+?)(ss|i)es$/;
- re2 = /^(.+?)([^s])s$/;
-
- if (re.test(w)) { w = w.replace(re,'$1$2'); }
- else if (re2.test(w)) { w = w.replace(re2,'$1$2'); }
-
- // Step 1b
- re = /^(.+?)eed$/;
- re2 = /^(.+?)(ed|ing)$/;
- if (re.test(w)) {
- const fp = re.exec(w);
- re = new RegExp(mgr0);
- if (re.test(fp[1])) {
- re = /.$/;
- w = w.replace(re,'');
- }
- } else if (re2.test(w)) {
- const fp = re2.exec(w);
- stem = fp[1];
- re2 = new RegExp(s_v);
- if (re2.test(stem)) {
- w = stem;
- re2 = /(at|bl|iz)$/;
- re3 = new RegExp('([^aeiouylsz])\\1$');
- re4 = new RegExp('^' + C + v + '[^aeiouwxy]$');
- if (re2.test(w)) { w = w + 'e'; }
- else if (re3.test(w)) { re = /.$/; w = w.replace(re,''); }
- else if (re4.test(w)) { w = w + 'e'; }
- }
- }
-
- // Step 1c
- re = /^(.+?)y$/;
- if (re.test(w)) {
- const fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(s_v);
- if (re.test(stem)) { w = stem + 'i'; }
- }
-
- // Step 2
- re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
- if (re.test(w)) {
- const fp = re.exec(w);
- stem = fp[1];
- suffix = fp[2];
- re = new RegExp(mgr0);
- if (re.test(stem)) {
- w = stem + step2list[suffix];
- }
- }
-
- // Step 3
- re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
- if (re.test(w)) {
- const fp = re.exec(w);
- stem = fp[1];
- suffix = fp[2];
- re = new RegExp(mgr0);
- if (re.test(stem)) {
- w = stem + step3list[suffix];
- }
- }
-
- // Step 4
- re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
- re2 = /^(.+?)(s|t)(ion)$/;
- if (re.test(w)) {
- const fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(mgr1);
- if (re.test(stem)) {
- w = stem;
- }
- } else if (re2.test(w)) {
- const fp = re2.exec(w);
- stem = fp[1] + fp[2];
- re2 = new RegExp(mgr1);
- if (re2.test(stem)) {
- w = stem;
- }
- }
-
- // Step 5
- re = /^(.+?)e$/;
- if (re.test(w)) {
- const fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(mgr1);
- re2 = new RegExp(meq1);
- re3 = new RegExp('^' + C + v + '[^aeiouwxy]$');
- if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {
- w = stem;
- }
- }
-
- re = /ll$/;
- re2 = new RegExp(mgr1);
- if (re.test(w) && re2.test(w)) {
- re = /.$/;
- w = w.replace(re,'');
- }
-
- // and turn initial Y back to y
-
- if (firstch == 'y') {
- w = firstch.toLowerCase() + w.substr(1);
- }
-
- return w;
- };
-})();
-
/**
* Parses the answerline, returning the acceptable, promptable, and rejectable answers.
@@ -586,7 +411,7 @@ function checkAnswer(answerline, givenAnswer) {
const parsedAnswerline = parseAnswerline(answerline);
if (!answerlineIsFormatted && parsedAnswerline.accept[0].length > 1 && givenAnswer.length === 1 && isNaN(givenAnswer))
- return ['reject', null];
+ return { directive: 'reject', directedPrompt: null };
for (const answer of parsedAnswerline.reject) {
const useStemmer = (stemmer(answer) !== stemmer(parsedAnswerline.accept[0]));
@@ -597,36 +422,40 @@ function checkAnswer(answerline, givenAnswer) {
if (!stringMatchesReference({ string: givenAnswer, reference: answer, strictness: 11, useStemmer }))
continue;
- return ['reject', null];
+ return { directive: 'reject', directedPrompt: null };
}
if (/[[(]accept either/i.test(answerline)) {
for (const answer of parsedAnswerline.accept[0].split(' ')) {
- if (answerWorks(answer, givenAnswer, answerlineIsFormatted))
- return ['accept', null];
+ if (answerWorks(answer, givenAnswer, answerlineIsFormatted)) {
+ return { directive: 'accept', directedPrompt: null };
+ }
}
}
for (const answer of parsedAnswerline.accept) {
- if (answerWorks(answer, givenAnswer, answerlineIsFormatted))
- return ['accept', null];
+ if (answerWorks(answer, givenAnswer, answerlineIsFormatted)) {
+ return { directive: 'accept', directedPrompt: null };
+ }
}
for (const answer of parsedAnswerline.prompt) {
const directedPrompt = answer[1];
- if (answerWorks(answer[0], givenAnswer, answerlineIsFormatted))
- return ['prompt', directedPrompt];
+ if (answerWorks(answer[0], givenAnswer, answerlineIsFormatted)) {
+ return { directive: 'prompt', directedPrompt: directedPrompt };
+ }
}
if (/prompt on (a )?partial/.test(answerline)) {
for (const answer of parsedAnswerline.accept[0].split(' ')) {
- if (answerWorks(answer, givenAnswer, answerlineIsFormatted))
- return ['prompt', null];
+ if (answerWorks(answer, givenAnswer, answerlineIsFormatted)) {
+ return { directive: 'prompt', directedPrompt: null };
+ }
}
}
- return ['reject', null];
+ return { directive: 'reject', directedPrompt: null };
}
-module.exports = { checkAnswer };
+export default checkAnswer;
diff --git a/server/ip-filter.js b/server/ip-filter.js
index 3d35b5a5a..0ba804ff5 100644
--- a/server/ip-filter.js
+++ b/server/ip-filter.js
@@ -1,17 +1,17 @@
-const ipFilter = require('express-ipfilter').IpFilter;
-const IpDeniedError = require('express-ipfilter').IpDeniedError;
+import expressIpfilter from 'express-ipfilter';
+const { IpFilter, IpDeniedError } = expressIpfilter;
+
const ips = [
'3.236.192.58',
'18.206.238.89',
'18.215.118.139',
];
-
const clientIp = (req, _res) => {
return req.headers['x-forwarded-for'] ? (req.headers['x-forwarded-for']).split(',')[0] : req.ip;
};
-const ipFilterMiddleware = ipFilter(ips, { mode: 'deny', log: false, detectIp: clientIp });
+const ipFilterMiddleware = IpFilter(ips, { mode: 'deny', log: false, detectIp: clientIp });
const ipFilterError = (err, req, res, _next) => {
if (err instanceof IpDeniedError) {
@@ -23,6 +23,4 @@ const ipFilterError = (err, req, res, _next) => {
}
};
-module.exports = {
- ipFilterMiddleware, ipFilterError
-};
+export { ipFilterMiddleware, ipFilterError };
diff --git a/server/quizbowl.js b/server/quizbowl.js
deleted file mode 100644
index 7806e3c7e..000000000
--- a/server/quizbowl.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const DEFAULT_MIN_YEAR = 2010;
-const DEFAULT_MAX_YEAR = 2023;
-
-const DIFFICULTIES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
-
-const CATEGORIES = ['Literature', 'History', 'Science', 'Fine Arts', 'Religion', 'Mythology', 'Philosophy', 'Social Science', 'Current Events', 'Geography', 'Other Academic', 'Trash'];
-const SUBCATEGORIES = [
- ['American Literature', 'British Literature', 'Classical Literature', 'European Literature', 'World Literature', 'Other Literature'],
- ['American History', 'Ancient History', 'European History', 'World History', 'Other History'],
- ['Biology', 'Chemistry', 'Physics', 'Math', 'Other Science'],
- ['Visual Fine Arts', 'Auditory Fine Arts', 'Other Fine Arts'],
- ['Religion'],
- ['Mythology'],
- ['Philosophy'],
- ['Social Science'],
- ['Current Events'],
- ['Geography'],
- ['Other Academic'],
- ['Trash']
-];
-const SUBCATEGORIES_FLATTENED = ['American Literature', 'British Literature', 'Classical Literature', 'European Literature', 'World Literature', 'Other Literature', 'American History', 'Ancient History', 'European History', 'World History', 'Other History', 'Biology', 'Chemistry', 'Physics', 'Math', 'Other Science', 'Visual Fine Arts', 'Auditory Fine Arts', 'Other Fine Arts', 'Religion', 'Mythology', 'Philosophy', 'Social Science', 'Current Events', 'Geography', 'Other Academic', 'Trash'];
-
-/**
- * @returns {Number} The number of points scored on a tossup.
- */
-function scoreTossup({ isCorrect, inPower, endOfQuestion, isPace = false }) {
- const powerValue = isPace ? 20 : 15;
- const negValue = isPace ? 0 : -5;
- return isCorrect ? (inPower ? powerValue : 10) : (endOfQuestion ? 0 : negValue);
-}
-
-module.exports = { DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR, DIFFICULTIES, CATEGORIES, SUBCATEGORIES, SUBCATEGORIES_FLATTENED, scoreTossup };
diff --git a/server/server.js b/server/server.js
index 1b4043e84..2a63e7e15 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,76 +1,58 @@
-if (process.env.NODE_ENV !== 'production') {
- require('dotenv').config();
-}
+import 'dotenv/config';
+
+import { ipFilterMiddleware, ipFilterError } from './ip-filter.js';
+import { createAndReturnRoom } from './TossupRoom.js';
+import { WEBSOCKET_MAX_PAYLOAD, COOKIE_MAX_AGE } from '../constants.js';
+import aboutRouter from '../routes/about.js';
+import apiRouter from '../routes/api.js';
+import apiDocsRouter from '../routes/api-docs.js';
+import authRouter from '../routes/auth.js';
+import backupsRouter from '../routes/backups.js';
+import bonusesRouter from '../routes/bonuses.js';
+import databaseRouter from '../routes/database.js';
+import multiplayerRouter from '../routes/multiplayer.js';
+import tossupsRouter from '../routes/tossups.js';
+import userRouter from '../routes/user.js';
+import indexRouter from '../routes/index.js';
+
+import cookieSession from 'cookie-session';
+import express, { json } from 'express';
+import { createServer } from 'http';
+import { v4 } from 'uuid';
+import { WebSocketServer } from 'ws';
-const express = require('express');
const app = express();
-const server = require('http').createServer(app);
+const server = createServer(app);
const port = process.env.PORT || 3000;
-const uuid = require('uuid');
-const WebSocket = require('ws');
-const wss = new WebSocket.Server({
+const wss = new WebSocketServer({
server,
- maxPayload: 1024 * 1024 * 1, // 1 MB
+ maxPayload: WEBSOCKET_MAX_PAYLOAD,
});
// See https://masteringjs.io/tutorials/express/query-parameters
// for why we use 'simple'
app.set('query parser', 'simple');
-app.use(express.json());
+app.use(json());
-const cookieSession = require('cookie-session');
app.use(cookieSession({
name: 'session',
keys: [process.env.SECRET_KEY_1 ?? 'secretKey1', process.env.SECRET_KEY_2 ?? 'secretKey2'],
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
+ maxAge: COOKIE_MAX_AGE,
}));
-const createDOMPurify = require('dompurify');
-const { JSDOM } = require('jsdom');
-const window = new JSDOM('').window;
-const DOMPurify = createDOMPurify(window);
-
-const { ipFilterMiddleware, ipFilterError } = require('./ip-filter');
app.use(ipFilterMiddleware);
app.use(ipFilterError);
-
-const TossupRoom = require('./TossupRoom');
-
-const rooms = {};
-const permanentRooms = ['hsquizbowl', 'collegequizbowl', 'literature', 'history', 'science', 'fine-arts'];
-
-for (const roomName of permanentRooms) {
- rooms[roomName] = new TossupRoom(roomName, true);
-}
-
-app.get('/api/multiplayer/room-list', (_req, res) => {
- const roomList = {};
- for (const roomName in rooms) {
- if (rooms[roomName].settings.public) {
- roomList[roomName] = [
- Object.keys(rooms[roomName].players).length,
- Object.keys(rooms[roomName].sockets).length,
- permanentRooms.includes(roomName),
- ];
- }
- }
-
- res.send(JSON.stringify(roomList));
-});
-
wss.on('connection', (ws) => {
let [roomName, userId, username] = ws.protocol.split('%%%');
- roomName = DOMPurify.sanitize(decodeURIComponent(roomName));
+ roomName = decodeURIComponent(roomName);
userId = decodeURIComponent(userId);
username = decodeURIComponent(username);
- userId = (userId === 'unknown') ? uuid.v4() : userId;
+ userId = (userId === 'unknown') ? v4() : userId;
- if (!Object.prototype.hasOwnProperty.call(rooms, roomName))
- rooms[roomName] = new TossupRoom(roomName, false);
-
- rooms[roomName].connection(ws, userId, username);
+ const room = createAndReturnRoom(roomName);
+ room.connection(ws, userId, username);
ws.on('error', (err) => {
if (err instanceof RangeError) {
@@ -83,22 +65,10 @@ wss.on('connection', (ws) => {
});
-/**
- * Redirects:
- */
-app.use('/users', (req, res) => {
- res.redirect(`/user${req.url}`);
-});
-
-
app.get('/robots.txt', (_req, res) => {
res.sendFile('robots.txt', { root: './client' });
});
-app.get('/*.html', (req, res) => {
- res.redirect(req.url.substring(0, req.url.length - 5));
-});
-
app.get('/react(-dom)?/umd/*.js', (req, res) => {
res.sendFile(req.url, { root: './node_modules' });
});
@@ -127,29 +97,37 @@ app.get('/*.ico', (req, res) => {
res.sendFile(req.url, { root: './client' });
});
+app.use('/about', aboutRouter);
+app.use('/api', apiRouter);
+app.use('/api-docs', apiDocsRouter);
+app.use('/auth', authRouter);
+app.use('/backups', backupsRouter);
+app.use('/bonuses', bonusesRouter);
+app.use('/db', databaseRouter);
+app.use('/multiplayer', multiplayerRouter);
+app.use('/tossups', tossupsRouter);
+app.use('/user', userRouter);
+app.use('/', indexRouter);
-app.use('/about', require('../routes/about'));
-app.use('/api', require('../routes/api'));
-app.use('/api-docs', require('../routes/api-docs'));
-app.use('/auth', require('../routes/auth'));
-app.use('/backups', require('../routes/backups'));
-app.use('/bonuses', require('../routes/bonuses'));
-app.use('/db', require('../routes/database'));
-app.use('/multiplayer', require('../routes/multiplayer'));
-app.use('/tossups', require('../routes/tossups'));
-app.use('/user', require('../routes/user'));
-app.use('/', require('../routes/index'));
-
+/**
+ * Redirects:
+ */
+app.get('/*.html', (req, res) => {
+ res.redirect(req.url.substring(0, req.url.length - 5));
+});
app.get('/database', (_req, res) => {
res.redirect('/db');
});
+app.use('/users', (req, res) => {
+ res.redirect(`/user${req.url}`);
+});
+
app.use((req, res) => {
res.sendFile(req.url, { root: './client' });
});
-
// listen on ipv4 instead of ipv6
server.listen({ port, host: '0.0.0.0' }, () => {
console.log(`listening at port=${port}`);
diff --git a/tests/database.test.js b/tests/database.test.js
index b28f0da82..6a56e11a9 100644
--- a/tests/database.test.js
+++ b/tests/database.test.js
@@ -1,4 +1,4 @@
-const { getQuery, getPacket, getSet, getRandomQuestions, getNumPackets, reportQuestion } = require('../database/questions');
+import { getQuery, getPacket, getSet, getRandomBonuses, getRandomTossups, getNumPackets, reportQuestion } from '../database/questions';
async function testTiming(count) {
const packetNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
@@ -33,11 +33,17 @@ async function testTiming(count) {
}
console.timeEnd('getSet');
- console.time('getRandomQuestions');
+ console.time('getRandomBonuses');
for (let i = 0; i < count; i++) {
- await getRandomQuestions({ questionType: 'bonus', verbose: false });
+ await getRandomBonuses();
}
- console.timeEnd('getRandomQuestions');
+ console.timeEnd('getRandomBonuses');
+
+ console.time('getRandomTossups');
+ for (let i = 0; i < count; i++) {
+ await getRandomTossups();
+ }
+ console.timeEnd('getRandomTossups');
console.time('reportQuestion');
for (let i = 0; i < count; i++) {
diff --git a/tests/scorer.test.js b/tests/scorer.test.js
index e9023100f..57963e424 100644
--- a/tests/scorer.test.js
+++ b/tests/scorer.test.js
@@ -1,6 +1,10 @@
-const scorer = require('../server/scorer.js');
+import checkAnswer from '../server/checkAnswer.js';
+import * as bcolors from '../bcolors.js';
+
+import { createRequire } from 'module';
+
+const require = createRequire(import.meta.url);
const tests = require('./scorer.test.json');
-const bcolors = require('../bcolors.js');
function errorText(text) { // colors text red
return `${bcolors.FAIL}${text}${bcolors.ENDC}`;
@@ -16,13 +20,13 @@ function testAnswerline(group) {
const givenAnswer = test.given;
const expectedDirectedPrompt = test.directedPrompt;
- const [result, directedPrompt] = scorer.checkAnswer(answerline, givenAnswer);
+ const { directive, directedPrompt } = checkAnswer(answerline, givenAnswer);
- const eqAnswer = expected === result;
+ const eqAnswer = expected === directive;
total++;
- console.assert(eqAnswer, errorText(`expected "${expected}" but got "${result}" for given answer "${givenAnswer}"`));
+ console.assert(eqAnswer, errorText(`expected "${expected}" but got "${directive}" for given answer "${givenAnswer}"`));
if (!eqAnswer) return;
if (expectedDirectedPrompt || directedPrompt) {