diff --git a/client/about/index.html b/client/about/index.html index 94067624f..f83bef2f8 100644 --- a/client/about/index.html +++ b/client/about/index.html @@ -130,6 +130,20 @@

Does it cost money to create an account?

Geoword is the only feature that costs money. All of the other features of QB Reader are completely free.

+

What do I do if someone is causing trouble in a room?

+

+ If a player in a public room is causing issues, such as spamming, advertising, or being expletive, you have a few options. +

+

Where can I request a set?

You can do so @@ -156,4 +170,4 @@

Packet List:

- + \ No newline at end of file diff --git a/client/multiplayer/room.jsx b/client/multiplayer/room.jsx index f4abb288b..6e465801e 100644 --- a/client/multiplayer/room.jsx +++ b/client/multiplayer/room.jsx @@ -11,9 +11,10 @@ import upsertPlayerItem from '../scripts/upsertPlayerItem.js'; const categoryManager = new CategoryManager(); let oldCategories = JSON.stringify(categoryManager.export()); let startingDifficulties = []; - +let ownerId = ''; let maxPacketNumber = 24; - +let globalPublic = true; +let muteList = []; /** * userId to player object */ @@ -50,20 +51,24 @@ socket.onmessage = function (event) { const data = JSON.parse(event.data); switch (data.type) { case 'buzz': return buzz(data); - case 'force-username': return forceUsername(data); case 'chat': return chat(data, false); case 'chat-live-update': return chat(data, true); case 'clear-stats': return clearStats(data); + case 'confirm-ban': return confirmBan(data); case 'connection-acknowledged': return connectionAcknowledged(data); case 'connection-acknowledged-query': return connectionAcknowledgedQuery(data); case 'connection-acknowledged-tossup': return connectionAcknowledgedTossup(data); + case 'enforcing-removal': return ackRemovedFromRoom(data); case 'end-of-set': return endOfSet(data); case 'error': return handleError(data); + case 'force-username': return forceUsername(data); case 'give-answer': return giveAnswer(data); case 'give-answer-live-update': return logGiveAnswer(data, true); + case 'initiated-vk': return vkInit(data); case 'join': return join(data); case 'leave': return leave(data); case 'lost-buzzer-race': return lostBuzzerRace(data); + case 'mute-player': return mutePlayer(data); case 'next': return next(data); case 'no-questions-found': return noQuestionsFound(data); case 'pause': return pause(data); @@ -78,6 +83,7 @@ socket.onmessage = function (event) { case 'set-year-range': return setYearRange(data); case 'skip': return next(data); case 'start': return next(data); + case 'successful-vk': return vkHandle(data); case 'timer-update': return updateTimerDisplay(data.timeRemaining); case 'toggle-lock': return toggleLock(data); case 'toggle-login-required': return toggleLoginRequired(data); @@ -91,6 +97,17 @@ socket.onmessage = function (event) { case 'update-question': return updateQuestion(data); } }; +// if a banned/kicked user tries to join a room they were removed from this is the response +function ackRemovedFromRoom ({ removalType }) { + if (removalType === 'kick') { + window.alert('You were kicked from this room by players, and cannot rejoin it.'); + } else { + window.alert('You were banned from this room by the room owner, and cannot rejoin it.'); + } + setTimeout(() => { + window.location.replace('../'); + }, 100); +} function buzz ({ userId, username }) { logEvent(username, 'buzzed'); @@ -106,6 +123,9 @@ function buzz ({ userId, username }) { } function chat ({ message, userId, username }, live = false) { + if (muteList.includes(userId)) { + return; + } if (!live && message === '') { document.getElementById('live-chat-' + userId).parentElement.remove(); return; @@ -141,14 +161,26 @@ function clearStats ({ userId }) { for (const field of ['celerity', 'negs', 'points', 'powers', 'tens', 'tuh', 'zeroes']) { players[userId][field] = 0; } - upsertPlayerItem(players[userId], USER_ID); + upsertPlayerItem(players[userId], USER_ID, ownerId, socket, globalPublic); sortPlayerListGroup(); } +function confirmBan ({ targetId, targetUsername }) { + if (targetId === USER_ID) { + window.alert('You were banned from this room by the room owner.'); + setTimeout(() => { + window.location.replace('../'); + }, 100); + } else { + logEvent(targetUsername + ' has been banned from this room.'); + } +} + function connectionAcknowledged ({ buzzedIn, canBuzz, isPermanent, + ownerId: serverOwnerId, players: messagePlayers, questionProgress, settings, @@ -164,11 +196,12 @@ function connectionAcknowledged ({ document.getElementById('private-chat-warning').innerHTML = 'This is a permanent room. Some settings have been restricted.'; } - Object.keys(messagePlayers).forEach(userId => { + ownerId = serverOwnerId; + for (const userId of Object.keys(messagePlayers)) { messagePlayers[userId].celerity = messagePlayers[userId].celerity.correct.average; players[userId] = messagePlayers[userId]; - upsertPlayerItem(players[userId], USER_ID); - }); + upsertPlayerItem(players[userId], USER_ID, ownerId, socket, globalPublic); + } sortPlayerListGroup(); switch (questionProgress) { @@ -204,6 +237,7 @@ function connectionAcknowledged ({ document.getElementById('toggle-login-required').disabled = settings.public; document.getElementById('toggle-timer').disabled = settings.public; document.getElementById('toggle-public').checked = settings.public; + globalPublic = settings.public; document.getElementById('reading-speed').value = settings.readingSpeed; document.getElementById('reading-speed-display').textContent = settings.readingSpeed; @@ -327,7 +361,7 @@ async function giveAnswer ({ celerity, directive, directedPrompt, givenAnswer, p players[userId].tuh++; players[userId].celerity = celerity; - upsertPlayerItem(players[userId], USER_ID); + upsertPlayerItem(players[userId], USER_ID, ownerId, socket, globalPublic); sortPlayerListGroup(); } @@ -358,7 +392,7 @@ function join ({ isNew, user, userId, username }) { if (isNew) { user.celerity = user.celerity.correct.average; - upsertPlayerItem(user, USER_ID); + upsertPlayerItem(user, USER_ID, ownerId, socket, globalPublic); sortPlayerListGroup(); players[userId] = user; } else { @@ -454,6 +488,17 @@ function lostBuzzerRace ({ username, userId }) { logEvent(username, 'lost the buzzer race'); if (userId === USER_ID) { document.getElementById('answer-input-group').classList.add('d-none'); } } +function mutePlayer ({ targetId, muteStatus }) { + if (muteStatus === 'Mute') { + if (!muteList.includes(targetId)) { + muteList.push(targetId); + } + } else { + if (muteList.includes(targetId)) { + muteList = muteList.filter(Id => Id !== targetId); + } + } +} function next ({ oldTossup, tossup: nextTossup, type, username }) { switch (type) { @@ -604,6 +649,16 @@ function setUsername ({ oldUsername, newUsername, userId }) { window.localStorage.setItem('multiplayer-username', username); document.getElementById('username').value = username; } + upsertPlayerItem(players[userId], USER_ID, ownerId, socket, globalPublic); +} + +function setYearRange ({ minYear, maxYear, username }) { + if (username) { logEvent(username, `changed the year range to ${minYear}-${maxYear}`); } + + $('#slider').slider('values', 0, minYear); + $('#slider').slider('values', 1, maxYear); + document.getElementById('year-range-a').textContent = minYear; + document.getElementById('year-range-b').textContent = maxYear; } function toggleLock ({ lock, username }) { @@ -667,11 +722,14 @@ function togglePublic ({ public: isPublic, username }) { document.getElementById('toggle-timer').disabled = isPublic; document.getElementById('toggle-timer').checked = true; document.getElementById('toggle-public').checked = isPublic; - + globalPublic = isPublic; if (isPublic) { document.getElementById('toggle-lock').checked = false; document.getElementById('toggle-login-required').checked = false; } + Object.keys(players).forEach((player) => { + upsertPlayerItem(players[player], USER_ID, ownerId, socket, globalPublic); + }); } function updateQuestion ({ word }) { @@ -687,13 +745,19 @@ function updateTimerDisplay (time) { document.querySelector('.timer .fraction').innerText = '.' + tenths; } -function setYearRange ({ minYear, maxYear, username }) { - if (username) { logEvent(username, `changed the year range to ${minYear}-${maxYear}`); } +function vkInit ({ targetUsername, threshold }) { + logEvent(`A votekick has been started against user ${targetUsername} and needs ${threshold} votes to suceed.`); +} - $('#slider').slider('values', 0, minYear); - $('#slider').slider('values', 1, maxYear); - document.getElementById('year-range-a').textContent = minYear; - document.getElementById('year-range-b').textContent = maxYear; +function vkHandle ({ targetUsername, targetId }) { + if (USER_ID === targetId) { + window.alert('You were vote kicked from this room by others.'); + setTimeout(() => { + window.location.replace('../'); + }, 100); + } else { + logEvent(targetUsername + ' has been vote kicked from this room.'); + } } document.getElementById('answer-form').addEventListener('submit', function (event) { diff --git a/client/multiplayer/room.min.js b/client/multiplayer/room.min.js index 178883461..ab3298430 100644 --- a/client/multiplayer/room.min.js +++ b/client/multiplayer/room.min.js @@ -1,5 +1,6 @@ -import questionStats from"../scripts/auth/question-stats.js";import api from"../scripts/api/index.js";import audio from"../audio/index.js";import CategoryManager from"../../quizbowl/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import{arrayToRange,createTossupCard,rangeToArray}from"../scripts/utilities/index.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";import upsertPlayerItem from"../scripts/upsertPlayerItem.js";const categoryManager=new CategoryManager;let oldCategories=JSON.stringify(categoryManager.export()),startingDifficulties=[],maxPacketNumber=24;/** +import questionStats from"../scripts/auth/question-stats.js";import api from"../scripts/api/index.js";import audio from"../audio/index.js";import CategoryManager from"../../quizbowl/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import{arrayToRange,createTossupCard,rangeToArray}from"../scripts/utilities/index.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";import upsertPlayerItem from"../scripts/upsertPlayerItem.js";const categoryManager=new CategoryManager;let oldCategories=JSON.stringify(categoryManager.export()),startingDifficulties=[],ownerId="",maxPacketNumber=24,globalPublic=!0,muteList=[];/** * userId to player object */const players={},ROOM_NAME=decodeURIComponent(window.location.pathname.substring(13));let tossup={},USER_ID=window.localStorage.getItem("USER_ID")||"unknown",username=window.localStorage.getItem("multiplayer-username")||api.getRandomName();const socket=new window.WebSocket(window.location.href.replace("http","ws")+(window.location.href.endsWith("?private=true")?"&":"?")+new URLSearchParams({roomName:ROOM_NAME,userId:USER_ID,username}).toString()),PING_INTERVAL_ID=setInterval(()=>socket.send(JSON.stringify({type:"ping"})),45e3);// Ping server every 45 seconds to prevent socket disconnection -socket.onclose=function(a){const{code:b}=a;3e3!==b&&window.alert("Disconnected from server"),clearInterval(PING_INTERVAL_ID)},socket.onmessage=function(a){const b=JSON.parse(a.data);switch(b.type){case"buzz":return buzz(b);case"force-username":return forceUsername(b);case"chat":return chat(b,!1);case"chat-live-update":return chat(b,!0);case"clear-stats":return clearStats(b);case"connection-acknowledged":return connectionAcknowledged(b);case"connection-acknowledged-query":return connectionAcknowledgedQuery(b);case"connection-acknowledged-tossup":return connectionAcknowledgedTossup(b);case"end-of-set":return endOfSet(b);case"error":return handleError(b);case"give-answer":return giveAnswer(b);case"give-answer-live-update":return logGiveAnswer(b,!0);case"join":return join(b);case"leave":return leave(b);case"lost-buzzer-race":return lostBuzzerRace(b);case"next":return next(b);case"no-questions-found":return noQuestionsFound(b);case"pause":return pause(b);case"reveal-answer":return revealAnswer(b);case"set-categories":return setCategories(b);case"set-difficulties":return setDifficulties(b);case"set-reading-speed":return setReadingSpeed(b);case"set-packet-numbers":return setPacketNumbers(b);case"set-strictness":return setStrictness(b);case"set-set-name":return setSetName(b);case"set-username":return setUsername(b);case"set-year-range":return setYearRange(b);case"skip":return next(b);case"start":return next(b);case"timer-update":return updateTimerDisplay(b.timeRemaining);case"toggle-lock":return toggleLock(b);case"toggle-login-required":return toggleLoginRequired(b);case"toggle-powermark-only":return togglePowermarkOnly(b);case"toggle-public":return togglePublic(b);case"toggle-rebuzz":return toggleRebuzz(b);case"toggle-select-by-set-name":return toggleSelectBySetName(b);case"toggle-skip":return toggleSkip(b);case"toggle-standard-only":return toggleStandardOnly(b);case"toggle-timer":return toggleTimer(b);case"update-question":return updateQuestion(b)}};function buzz({userId:a,username:b}){logEvent(b,"buzzed"),document.getElementById("buzz").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("skip").disabled=!0,a===USER_ID&&(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus())}function chat({message:a,userId:c,username:d},e=!1){if(!e&&""===a)return void document.getElementById("live-chat-"+c).parentElement.remove();if(!e&&a)return document.getElementById("live-chat-"+c).className="",void(document.getElementById("live-chat-"+c).id="");if(document.getElementById("live-chat-"+c))return void(document.getElementById("live-chat-"+c).textContent=a);const f=document.createElement("b");f.textContent=d;const b=document.createElement("span");b.classList.add("text-muted"),b.id="live-chat-"+c,b.textContent=a;const g=document.createElement("li");g.appendChild(f),g.appendChild(document.createTextNode(" ")),g.appendChild(b),document.getElementById("room-history").prepend(g)}function clearStats({userId:a}){for(const b of["celerity","negs","points","powers","tens","tuh","zeroes"])players[a][b]=0;upsertPlayerItem(players[a],USER_ID),sortPlayerListGroup()}function connectionAcknowledged({buzzedIn:a,canBuzz:b,isPermanent:c,players:d,questionProgress:e,settings:f,userId:g}){document.getElementById("buzz").disabled=!b,c&&(document.getElementById("category-select-button").disabled=!0,document.getElementById("strictness").disabled=!0,document.getElementById("toggle-public").disabled=!0,document.getElementById("toggle-select-by-set-name").disabled=!0,document.getElementById("private-chat-warning").innerHTML="This is a permanent room. Some settings have been restricted."),Object.keys(d).forEach(a=>{d[a].celerity=d[a].celerity.correct.average,players[a]=d[a],upsertPlayerItem(players[a],USER_ID)}),sortPlayerListGroup();0===e?(document.getElementById("next").textContent="Start",document.getElementById("next").classList.remove("btn-primary"),document.getElementById("next").classList.add("btn-success")):1===e?(showSkipButton(),document.getElementById("settings").classList.add("d-none"),a?(document.getElementById("buzz").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("pause").disabled=!0):(document.getElementById("buzz").disabled=!1,document.getElementById("pause").disabled=!1)):2===e?(showNextButton(),document.getElementById("settings").classList.add("d-none")):void 0;document.getElementById("toggle-lock").checked=f.lock,document.getElementById("toggle-login-required").checked=f.loginRequired,document.getElementById("chat").disabled=f.public,document.getElementById("toggle-lock").disabled=f.public,document.getElementById("toggle-login-required").disabled=f.public,document.getElementById("toggle-timer").disabled=f.public,document.getElementById("toggle-public").checked=f.public,document.getElementById("reading-speed").value=f.readingSpeed,document.getElementById("reading-speed-display").textContent=f.readingSpeed,document.getElementById("strictness").value=f.strictness,document.getElementById("strictness-display").textContent=f.strictness,document.getElementById("toggle-rebuzz").checked=f.rebuzz,document.getElementById("toggle-skip").checked=f.skip,document.getElementById("timer").classList.toggle("d-none",!f.timer),document.getElementById("toggle-timer").checked=f.timer,USER_ID=g,window.localStorage.setItem("USER_ID",USER_ID)}async function connectionAcknowledgedQuery({difficulties:k=[],minYear:a,maxYear:b,packetNumbers:l=[],powermarkOnly:c,selectBySetName:d,setName:m="",standardOnly:e,alternateSubcategories:f,categories:g,subcategories:h,percentView:i,categoryPercents:j}){setDifficulties({difficulties:k}),$("#slider").slider("values",0,a),$("#slider").slider("values",1,b),document.getElementById("year-range-a").textContent=a,document.getElementById("year-range-b").textContent=b,document.getElementById("packet-number").value=arrayToRange(l),document.getElementById("toggle-powermark-only").checked=c,document.getElementById("difficulty-settings").classList.toggle("d-none",d),document.getElementById("set-settings").classList.toggle("d-none",!d),document.getElementById("toggle-select-by-set-name").checked=d,document.getElementById("toggle-powermark-only").disabled=d,document.getElementById("toggle-standard-only").disabled=d,document.getElementById("set-name").value=m,maxPacketNumber=await api.getNumPackets(m),""!==m&&0===maxPacketNumber&&document.getElementById("set-name").classList.add("is-invalid"),document.getElementById("toggle-standard-only").checked=e,categoryManager.import({categories:g,subcategories:h,alternateSubcategories:f,percentView:i,categoryPercents:j}),categoryManager.loadCategoryModal()}function connectionAcknowledgedTossup({tossup:a}){tossup=a,document.getElementById("set-name-info").textContent=tossup?.set?.name??"",document.getElementById("packet-number-info").textContent=tossup?.packet?.number??"-",document.getElementById("question-number-info").textContent=tossup?.number??"-"}function endOfSet(){window.alert("You have reached the end of the set")}function forceUsername({message:a,username:b}){window.alert(a),window.localStorage.setItem("multiplayer-username",b),document.querySelector("#username").value=b}async function giveAnswer({celerity:a,directive:b,directedPrompt:c,givenAnswer:d,perQuestionCelerity:e,score:f,tossup:g,userId:h,username:i}){document.getElementById("answer-input").value="",document.getElementById("answer-input-group").classList.add("d-none"),document.getElementById("answer-input").blur(),logGiveAnswer({directive:b,message:d,username:i}),"prompt"===b&&c?logEvent(i,`was prompted with "${c}"`):"prompt"===b?logEvent(i,"was prompted"):logEvent(i,`${0{a.textContent=parseInt(a.innerHTML)+1})),"reject"===b&&(document.getElementById("buzz").disabled=!document.getElementById("toggle-rebuzz").checked&&h===USER_ID),10f&&players[h].negs++,players[h].points+=f,players[h].tuh++,players[h].celerity=a,upsertPlayerItem(players[h],USER_ID),sortPlayerListGroup()),"prompt"!==b&&h===USER_ID&&questionStats.recordTossup(g,0{const b=parseInt(document.getElementById("points-"+d.id.substring(f)).innerHTML),e=parseInt(document.getElementById("points-"+a.id.substring(f)).innerHTML);// if points are equal, sort alphabetically by username -if(b===e){const b=document.getElementById("username-"+d.id.substring(f)).innerHTML,e=document.getElementById("username-"+a.id.substring(f)).innerHTML;return c?b.localeCompare(e):e.localeCompare(b)}return c?e-b:b-e}).forEach(a=>{d.appendChild(a)})}function setCategories({alternateSubcategories:a,categories:b,subcategories:c,percentView:d,categoryPercents:e,username:f}){logEvent(f,"updated the categories"),categoryManager.import({categories:b,subcategories:c,alternateSubcategories:a,percentView:d,categoryPercents:e}),categoryManager.loadCategoryModal()}function setDifficulties({difficulties:a,username:b=void 0}){return b&&logEvent(b,0{const c=b.querySelector("input");a.includes(parseInt(c.value))?(c.checked=!0,b.classList.add("active")):(c.checked=!1,b.classList.remove("active"))}):void(startingDifficulties=a)}function setPacketNumbers({username:a,packetNumbers:b}){b=arrayToRange(b),logEvent(a,01>a||a>maxPacketNumber)?void document.getElementById("packet-number").classList.add("is-invalid"):void(document.getElementById("packet-number").classList.remove("is-invalid"),socket.send(JSON.stringify({type:"set-packet-numbers",packetNumbers:a})))}),document.getElementById("pause").addEventListener("click",function(){this.blur();const a=parseFloat(document.querySelector(".timer .face").innerText),b=parseFloat(document.querySelector(".timer .fraction").innerText);socket.send(JSON.stringify({type:"pause",pausedTime:10*(a+b)}))}),document.getElementById("reading-speed").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-reading-speed",readingSpeed:this.value}))}),document.getElementById("reading-speed").addEventListener("input",function(){document.getElementById("reading-speed-display").textContent=this.value}),document.getElementById("report-question-submit").addEventListener("click",function(){api.reportQuestion(document.getElementById("report-question-id").value,document.getElementById("report-question-reason").value,document.getElementById("report-question-description").value)}),document.getElementById("set-name").addEventListener("change",async function(){api.getSetList().includes(this.value)||0===this.value.length?this.classList.remove("is-invalid"):this.classList.add("is-invalid"),maxPacketNumber=await api.getNumPackets(this.value),document.getElementById("packet-number").value=""===this.value||0===maxPacketNumber?"":`1-${maxPacketNumber}`,socket.send(JSON.stringify({type:"set-set-name",setName:this.value,packetNumbers:rangeToArray(document.getElementById("packet-number").value)}))}),document.getElementById("strictness").addEventListener("change",function(){this.blur(),socket.send(JSON.stringify({type:"set-strictness",strictness:this.value}))}),document.getElementById("strictness").addEventListener("input",function(){document.getElementById("strictness-display").textContent=this.value}),document.getElementById("toggle-lock").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-lock",lock:this.checked}))}),document.getElementById("toggle-login-required").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-login-required",loginRequired:this.checked}))}),document.getElementById("toggle-powermark-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-powermark-only",powermarkOnly:this.checked}))}),document.getElementById("toggle-rebuzz").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-rebuzz",rebuzz:this.checked}))}),document.getElementById("toggle-skip").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-skip",skip:this.checked}))}),document.getElementById("toggle-select-by-set-name").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-select-by-set-name",setName:document.getElementById("set-name").value,selectBySetName:this.checked}))}),document.getElementById("toggle-settings").addEventListener("click",function(){this.blur(),document.getElementById("buttons").classList.toggle("col-lg-9"),document.getElementById("buttons").classList.toggle("col-lg-12"),document.getElementById("content").classList.toggle("col-lg-9"),document.getElementById("content").classList.toggle("col-lg-12"),document.getElementById("settings").classList.toggle("d-none"),document.getElementById("settings").classList.toggle("d-lg-none")}),document.getElementById("toggle-standard-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-standard-only",standardOnly:this.checked}))}),document.getElementById("toggle-timer").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-timer",timer:this.checked}))}),document.getElementById("toggle-public").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-public",public:this.checked}))}),document.getElementById("username").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-username",userId:USER_ID,username:this.value})),username=this.value,window.localStorage.setItem("multiplayer-username",username)}),document.getElementById("year-range-a").onchange=function(){const[a,b]=$("#slider").slider("values");if(b{if("Escape"===a.key&&"chat-input"===document.activeElement.id&&(document.getElementById("chat-input").value="",document.getElementById("chat-input-group").classList.add("d-none"),document.getElementById("chat-input").blur(),socket.send(JSON.stringify({type:"chat",message:""}))),!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName))switch(a.key?.toLowerCase()){case" ":document.getElementById("buzz").click(),a.target===document.body&&a.preventDefault();break;case"e":return document.getElementById("toggle-settings").click();case"k":return document.getElementsByClassName("card-header-clickable")[0].click();case"p":return document.getElementById("pause").click();case"t":return document.getElementsByClassName("star-tossup")[0].click();case"y":return navigator.clipboard.writeText(tossup._id??"");case"n":case"s":document.getElementById("next").click(),document.getElementById("skip").click()}}),document.addEventListener("keypress",function(a){"Enter"===a.key&&a.target===document.body&&document.getElementById("chat").click()}),document.getElementById("username").value=username,ReactDOM.createRoot(document.getElementById("category-modal-root")).render(/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,onClose:()=>{oldCategories!==JSON.stringify(categoryManager.export())&&socket.send(JSON.stringify({type:"set-categories",...categoryManager.export()})),oldCategories=JSON.stringify(categoryManager.export())}})),ReactDOM.createRoot(document.getElementById("difficulty-dropdown-root")).render(/*#__PURE__*/React.createElement(DifficultyDropdown,{startingDifficulties:startingDifficulties,onChange:()=>socket.send(JSON.stringify({type:"set-difficulties",difficulties:getDropdownValues("difficulties")}))})); \ No newline at end of file +socket.onclose=function(a){const{code:b}=a;3e3!==b&&window.alert("Disconnected from server"),clearInterval(PING_INTERVAL_ID)},socket.onmessage=function(a){const b=JSON.parse(a.data);switch(b.type){case"buzz":return buzz(b);case"chat":return chat(b,!1);case"chat-live-update":return chat(b,!0);case"clear-stats":return clearStats(b);case"confirm-ban":return confirmBan(b);case"connection-acknowledged":return connectionAcknowledged(b);case"connection-acknowledged-query":return connectionAcknowledgedQuery(b);case"connection-acknowledged-tossup":return connectionAcknowledgedTossup(b);case"enforcing-removal":return ackRemovedFromRoom(b);case"end-of-set":return endOfSet(b);case"error":return handleError(b);case"force-username":return forceUsername(b);case"give-answer":return giveAnswer(b);case"give-answer-live-update":return logGiveAnswer(b,!0);case"initiated-vk":return vkInit(b);case"join":return join(b);case"leave":return leave(b);case"lost-buzzer-race":return lostBuzzerRace(b);case"mute-player":return mutePlayer(b);case"next":return next(b);case"no-questions-found":return noQuestionsFound(b);case"pause":return pause(b);case"reveal-answer":return revealAnswer(b);case"set-categories":return setCategories(b);case"set-difficulties":return setDifficulties(b);case"set-reading-speed":return setReadingSpeed(b);case"set-packet-numbers":return setPacketNumbers(b);case"set-strictness":return setStrictness(b);case"set-set-name":return setSetName(b);case"set-username":return setUsername(b);case"set-year-range":return setYearRange(b);case"skip":return next(b);case"start":return next(b);case"successful-vk":return vkHandle(b);case"timer-update":return updateTimerDisplay(b.timeRemaining);case"toggle-lock":return toggleLock(b);case"toggle-login-required":return toggleLoginRequired(b);case"toggle-powermark-only":return togglePowermarkOnly(b);case"toggle-public":return togglePublic(b);case"toggle-rebuzz":return toggleRebuzz(b);case"toggle-select-by-set-name":return toggleSelectBySetName(b);case"toggle-skip":return toggleSkip(b);case"toggle-standard-only":return toggleStandardOnly(b);case"toggle-timer":return toggleTimer(b);case"update-question":return updateQuestion(b)}};// if a banned/kicked user tries to join a room they were removed from this is the response +function ackRemovedFromRoom({removalType:a}){"kick"===a?window.alert("You were kicked from this room by players, and cannot rejoin it."):window.alert("You were banned from this room by the room owner, and cannot rejoin it."),setTimeout(()=>{window.location.replace("../")},100)}function buzz({userId:a,username:b}){logEvent(b,"buzzed"),document.getElementById("buzz").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("skip").disabled=!0,a===USER_ID&&(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus())}function chat({message:a,userId:c,username:d},e=!1){if(!muteList.includes(c)){if(!e&&""===a)return void document.getElementById("live-chat-"+c).parentElement.remove();if(!e&&a)return document.getElementById("live-chat-"+c).className="",void(document.getElementById("live-chat-"+c).id="");if(document.getElementById("live-chat-"+c))return void(document.getElementById("live-chat-"+c).textContent=a);const f=document.createElement("b");f.textContent=d;const b=document.createElement("span");b.classList.add("text-muted"),b.id="live-chat-"+c,b.textContent=a;const g=document.createElement("li");g.appendChild(f),g.appendChild(document.createTextNode(" ")),g.appendChild(b),document.getElementById("room-history").prepend(g)}}function clearStats({userId:a}){for(const b of["celerity","negs","points","powers","tens","tuh","zeroes"])players[a][b]=0;upsertPlayerItem(players[a],USER_ID,ownerId,socket,globalPublic),sortPlayerListGroup()}function confirmBan({targetId:a,targetUsername:b}){a===USER_ID?(window.alert("You were banned from this room by the room owner."),setTimeout(()=>{window.location.replace("../")},100)):logEvent(b+" has been banned from this room.")}function connectionAcknowledged({buzzedIn:a,canBuzz:b,isPermanent:c,ownerId:d,players:e,questionProgress:f,settings:g,userId:h}){document.getElementById("buzz").disabled=!b,c&&(document.getElementById("category-select-button").disabled=!0,document.getElementById("strictness").disabled=!0,document.getElementById("toggle-public").disabled=!0,document.getElementById("toggle-select-by-set-name").disabled=!0,document.getElementById("private-chat-warning").innerHTML="This is a permanent room. Some settings have been restricted."),ownerId=d;for(const i of Object.keys(e))e[i].celerity=e[i].celerity.correct.average,players[i]=e[i],upsertPlayerItem(players[i],USER_ID,ownerId,socket,globalPublic);sortPlayerListGroup();0===f?(document.getElementById("next").textContent="Start",document.getElementById("next").classList.remove("btn-primary"),document.getElementById("next").classList.add("btn-success")):1===f?(showSkipButton(),document.getElementById("settings").classList.add("d-none"),a?(document.getElementById("buzz").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("pause").disabled=!0):(document.getElementById("buzz").disabled=!1,document.getElementById("pause").disabled=!1)):2===f?(showNextButton(),document.getElementById("settings").classList.add("d-none")):void 0;document.getElementById("toggle-lock").checked=g.lock,document.getElementById("toggle-login-required").checked=g.loginRequired,document.getElementById("chat").disabled=g.public,document.getElementById("toggle-lock").disabled=g.public,document.getElementById("toggle-login-required").disabled=g.public,document.getElementById("toggle-timer").disabled=g.public,document.getElementById("toggle-public").checked=g.public,globalPublic=g.public,document.getElementById("reading-speed").value=g.readingSpeed,document.getElementById("reading-speed-display").textContent=g.readingSpeed,document.getElementById("strictness").value=g.strictness,document.getElementById("strictness-display").textContent=g.strictness,document.getElementById("toggle-rebuzz").checked=g.rebuzz,document.getElementById("toggle-skip").checked=g.skip,document.getElementById("timer").classList.toggle("d-none",!g.timer),document.getElementById("toggle-timer").checked=g.timer,USER_ID=h,window.localStorage.setItem("USER_ID",USER_ID)}async function connectionAcknowledgedQuery({difficulties:k=[],minYear:a,maxYear:b,packetNumbers:l=[],powermarkOnly:c,selectBySetName:d,setName:m="",standardOnly:e,alternateSubcategories:f,categories:g,subcategories:h,percentView:i,categoryPercents:j}){setDifficulties({difficulties:k}),$("#slider").slider("values",0,a),$("#slider").slider("values",1,b),document.getElementById("year-range-a").textContent=a,document.getElementById("year-range-b").textContent=b,document.getElementById("packet-number").value=arrayToRange(l),document.getElementById("toggle-powermark-only").checked=c,document.getElementById("difficulty-settings").classList.toggle("d-none",d),document.getElementById("set-settings").classList.toggle("d-none",!d),document.getElementById("toggle-select-by-set-name").checked=d,document.getElementById("toggle-powermark-only").disabled=d,document.getElementById("toggle-standard-only").disabled=d,document.getElementById("set-name").value=m,maxPacketNumber=await api.getNumPackets(m),""!==m&&0===maxPacketNumber&&document.getElementById("set-name").classList.add("is-invalid"),document.getElementById("toggle-standard-only").checked=e,categoryManager.import({categories:g,subcategories:h,alternateSubcategories:f,percentView:i,categoryPercents:j}),categoryManager.loadCategoryModal()}function connectionAcknowledgedTossup({tossup:a}){tossup=a,document.getElementById("set-name-info").textContent=tossup?.set?.name??"",document.getElementById("packet-number-info").textContent=tossup?.packet?.number??"-",document.getElementById("question-number-info").textContent=tossup?.number??"-"}function endOfSet(){window.alert("You have reached the end of the set")}function forceUsername({message:a,username:b}){window.alert(a),window.localStorage.setItem("multiplayer-username",b),document.querySelector("#username").value=b}async function giveAnswer({celerity:a,directive:b,directedPrompt:c,givenAnswer:d,perQuestionCelerity:e,score:f,tossup:g,userId:h,username:i}){document.getElementById("answer-input").value="",document.getElementById("answer-input-group").classList.add("d-none"),document.getElementById("answer-input").blur(),logGiveAnswer({directive:b,message:d,username:i}),"prompt"===b&&c?logEvent(i,`was prompted with "${c}"`):"prompt"===b?logEvent(i,"was prompted"):logEvent(i,`${0{a.textContent=parseInt(a.innerHTML)+1})),"reject"===b&&(document.getElementById("buzz").disabled=!document.getElementById("toggle-rebuzz").checked&&h===USER_ID),10f&&players[h].negs++,players[h].points+=f,players[h].tuh++,players[h].celerity=a,upsertPlayerItem(players[h],USER_ID,ownerId,socket,globalPublic),sortPlayerListGroup()),"prompt"!==b&&h===USER_ID&&questionStats.recordTossup(g,0b!==a))}function next({oldTossup:a,tossup:b,type:c,username:d}){switch(c){case"next":logEvent(d,"went to the next question");break;case"skip":logEvent(d,"skipped the question");break;case"start":logEvent(d,"started the game");break;default:throw new Error("Invalid type")}"next"===c||"skip"===c?createTossupCard(a):"start"===c&&(document.getElementById("next").classList.add("btn-primary"),document.getElementById("next").classList.remove("btn-success"),document.getElementById("next").textContent="Next"),tossup=b,document.getElementById("packet-number-info").textContent=tossup?.packet.number??"-",document.getElementById("question-number-info").textContent=tossup?.number??"-",document.getElementById("set-name-info").textContent=tossup?.set.name??"",document.getElementById("answer").textContent="",document.getElementById("question").textContent="",document.getElementById("buzz").textContent="Buzz",document.getElementById("buzz").disabled=!1,document.getElementById("pause").textContent="Pause",document.getElementById("pause").disabled=!1,document.getElementById("settings").classList.add("d-none"),showSkipButton(),updateTimerDisplay(100)}function noQuestionsFound(){window.alert("No questions found")}function pause({paused:a,username:b}){logEvent(b,`${a?"":"un"}paused the game`)}function revealAnswer({answer:a,question:b}){document.getElementById("question").innerHTML=b,document.getElementById("answer").innerHTML="ANSWER: "+a,document.getElementById("pause").disabled=!0,showNextButton()}function showNextButton(){document.getElementById("next").classList.remove("d-none"),document.getElementById("next").disabled=!1,document.getElementById("skip").classList.add("d-none"),document.getElementById("skip").disabled=!0}function showSkipButton(){document.getElementById("skip").classList.remove("d-none"),document.getElementById("skip").disabled=!document.getElementById("toggle-skip").checked,document.getElementById("next").classList.add("d-none"),document.getElementById("next").disabled=!0}function sortPlayerListGroup(c=!0){const d=document.getElementById("player-list-group"),e=Array.from(d.children),f=11;e.sort((d,a)=>{const b=parseInt(document.getElementById("points-"+d.id.substring(f)).innerHTML),e=parseInt(document.getElementById("points-"+a.id.substring(f)).innerHTML);// if points are equal, sort alphabetically by username +if(b===e){const b=document.getElementById("username-"+d.id.substring(f)).innerHTML,e=document.getElementById("username-"+a.id.substring(f)).innerHTML;return c?b.localeCompare(e):e.localeCompare(b)}return c?e-b:b-e}).forEach(a=>{d.appendChild(a)})}function setCategories({alternateSubcategories:a,categories:b,subcategories:c,percentView:d,categoryPercents:e,username:f}){logEvent(f,"updated the categories"),categoryManager.import({categories:b,subcategories:c,alternateSubcategories:a,percentView:d,categoryPercents:e}),categoryManager.loadCategoryModal()}function setDifficulties({difficulties:a,username:b=void 0}){return b&&logEvent(b,0{const c=b.querySelector("input");a.includes(parseInt(c.value))?(c.checked=!0,b.classList.add("active")):(c.checked=!1,b.classList.remove("active"))}):void(startingDifficulties=a)}function setPacketNumbers({username:a,packetNumbers:b}){b=arrayToRange(b),logEvent(a,0{upsertPlayerItem(players[a],USER_ID,ownerId,socket,globalPublic)})}function updateQuestion({word:a}){"(*)"===a||(document.getElementById("question").innerHTML+=a+" ")}function updateTimerDisplay(a){const b=Math.floor(a/10);document.querySelector(".timer .face").innerText=b,document.querySelector(".timer .fraction").innerText="."+a%10}function vkInit({targetUsername:a,threshold:b}){logEvent(`A votekick has been started against user ${a} and needs ${b} votes to suceed.`)}function vkHandle({targetUsername:a,targetId:b}){USER_ID===b?(window.alert("You were vote kicked from this room by others."),setTimeout(()=>{window.location.replace("../")},100)):logEvent(a+" has been vote kicked from this room.")}document.getElementById("answer-form").addEventListener("submit",function(a){a.preventDefault(),a.stopPropagation();const b=document.getElementById("answer-input").value;socket.send(JSON.stringify({type:"give-answer",givenAnswer:b}))}),document.getElementById("answer-input").addEventListener("input",function(){socket.send(JSON.stringify({type:"give-answer-live-update",message:this.value}))}),document.getElementById("buzz").addEventListener("click",function(){this.blur(),audio.soundEffects&&audio.buzz.play(),socket.send(JSON.stringify({type:"buzz"})),socket.send(JSON.stringify({type:"give-answer-live-update",message:""}))}),document.getElementById("chat").addEventListener("click",function(){this.blur(),document.getElementById("chat-input-group").classList.remove("d-none"),document.getElementById("chat-input").focus(),socket.send(JSON.stringify({type:"chat-live-update",message:""}))}),document.getElementById("chat-form").addEventListener("submit",function(a){a.preventDefault(),a.stopPropagation();const b=document.getElementById("chat-input").value;document.getElementById("chat-input").value="",document.getElementById("chat-input-group").classList.add("d-none"),document.getElementById("chat-input").blur(),socket.send(JSON.stringify({type:"chat",message:b}))}),document.getElementById("chat-input").addEventListener("input",function(){socket.send(JSON.stringify({type:"chat-live-update",message:this.value}))}),document.getElementById("clear-stats").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"clear-stats"}))}),document.getElementById("next").addEventListener("click",function(){switch(this.blur(),this.innerHTML){case"Start":socket.send(JSON.stringify({type:"start"}));break;case"Next":socket.send(JSON.stringify({type:"next"}))}}),document.getElementById("skip").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"skip"}))}),document.getElementById("packet-number").addEventListener("change",function(){const a=rangeToArray(this.value,maxPacketNumber);return a.some(a=>1>a||a>maxPacketNumber)?void document.getElementById("packet-number").classList.add("is-invalid"):void(document.getElementById("packet-number").classList.remove("is-invalid"),socket.send(JSON.stringify({type:"set-packet-numbers",packetNumbers:a})))}),document.getElementById("pause").addEventListener("click",function(){this.blur();const a=parseFloat(document.querySelector(".timer .face").innerText),b=parseFloat(document.querySelector(".timer .fraction").innerText);socket.send(JSON.stringify({type:"pause",pausedTime:10*(a+b)}))}),document.getElementById("reading-speed").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-reading-speed",readingSpeed:this.value}))}),document.getElementById("reading-speed").addEventListener("input",function(){document.getElementById("reading-speed-display").textContent=this.value}),document.getElementById("report-question-submit").addEventListener("click",function(){api.reportQuestion(document.getElementById("report-question-id").value,document.getElementById("report-question-reason").value,document.getElementById("report-question-description").value)}),document.getElementById("set-name").addEventListener("change",async function(){api.getSetList().includes(this.value)||0===this.value.length?this.classList.remove("is-invalid"):this.classList.add("is-invalid"),maxPacketNumber=await api.getNumPackets(this.value),document.getElementById("packet-number").value=""===this.value||0===maxPacketNumber?"":`1-${maxPacketNumber}`,socket.send(JSON.stringify({type:"set-set-name",setName:this.value,packetNumbers:rangeToArray(document.getElementById("packet-number").value)}))}),document.getElementById("strictness").addEventListener("change",function(){this.blur(),socket.send(JSON.stringify({type:"set-strictness",strictness:this.value}))}),document.getElementById("strictness").addEventListener("input",function(){document.getElementById("strictness-display").textContent=this.value}),document.getElementById("toggle-lock").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-lock",lock:this.checked}))}),document.getElementById("toggle-login-required").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-login-required",loginRequired:this.checked}))}),document.getElementById("toggle-powermark-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-powermark-only",powermarkOnly:this.checked}))}),document.getElementById("toggle-rebuzz").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-rebuzz",rebuzz:this.checked}))}),document.getElementById("toggle-skip").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-skip",skip:this.checked}))}),document.getElementById("toggle-select-by-set-name").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-select-by-set-name",setName:document.getElementById("set-name").value,selectBySetName:this.checked}))}),document.getElementById("toggle-settings").addEventListener("click",function(){this.blur(),document.getElementById("buttons").classList.toggle("col-lg-9"),document.getElementById("buttons").classList.toggle("col-lg-12"),document.getElementById("content").classList.toggle("col-lg-9"),document.getElementById("content").classList.toggle("col-lg-12"),document.getElementById("settings").classList.toggle("d-none"),document.getElementById("settings").classList.toggle("d-lg-none")}),document.getElementById("toggle-standard-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-standard-only",standardOnly:this.checked}))}),document.getElementById("toggle-timer").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-timer",timer:this.checked}))}),document.getElementById("toggle-public").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-public",public:this.checked}))}),document.getElementById("username").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-username",userId:USER_ID,username:this.value})),username=this.value,window.localStorage.setItem("multiplayer-username",username)}),document.getElementById("year-range-a").onchange=function(){const[a,b]=$("#slider").slider("values");if(b{if("Escape"===a.key&&"chat-input"===document.activeElement.id&&(document.getElementById("chat-input").value="",document.getElementById("chat-input-group").classList.add("d-none"),document.getElementById("chat-input").blur(),socket.send(JSON.stringify({type:"chat",message:""}))),!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName))switch(a.key?.toLowerCase()){case" ":document.getElementById("buzz").click(),a.target===document.body&&a.preventDefault();break;case"e":return document.getElementById("toggle-settings").click();case"k":return document.getElementsByClassName("card-header-clickable")[0].click();case"p":return document.getElementById("pause").click();case"t":return document.getElementsByClassName("star-tossup")[0].click();case"y":return navigator.clipboard.writeText(tossup._id??"");case"n":case"s":document.getElementById("next").click(),document.getElementById("skip").click()}}),document.addEventListener("keypress",function(a){"Enter"===a.key&&a.target===document.body&&document.getElementById("chat").click()}),document.getElementById("username").value=username,ReactDOM.createRoot(document.getElementById("category-modal-root")).render(/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,onClose:()=>{oldCategories!==JSON.stringify(categoryManager.export())&&socket.send(JSON.stringify({type:"set-categories",...categoryManager.export()})),oldCategories=JSON.stringify(categoryManager.export())}})),ReactDOM.createRoot(document.getElementById("difficulty-dropdown-root")).render(/*#__PURE__*/React.createElement(DifficultyDropdown,{startingDifficulties:startingDifficulties,onChange:()=>socket.send(JSON.stringify({type:"set-difficulties",difficulties:getDropdownValues("difficulties")}))})); \ No newline at end of file diff --git a/client/scripts/upsertPlayerItem.js b/client/scripts/upsertPlayerItem.js index f9579406a..7351fc7ce 100644 --- a/client/scripts/upsertPlayerItem.js +++ b/client/scripts/upsertPlayerItem.js @@ -4,11 +4,21 @@ import { escapeHTML } from './utilities/strings.js'; * Upserts a player item to the DOM element with the id `player-list-group`. * @param {Player} player * @param {string} USER_ID - The item is highlighted blue if `USER_ID === player.userId`. + * @param {string} ownerId - ID of the room owner */ -export default function upsertPlayerItem (player, USER_ID) { +// overall handling of some of these mechanics in the upsertion section might not be best idea? works though +export default function upsertPlayerItem (player, USER_ID, ownerId, socket, isPublic) { + if (!player || !player.userId) { + console.error('Player or player.userId is undefined', { player }); + return; + } + const { userId, username, powers = 0, tens = 0, negs = 0, tuh = 0, points = 0, online } = player; const celerity = player?.celerity?.correct?.average ?? player?.celerity ?? 0; + const playerIsOwner = ownerId === userId; + + // Remove the existing player item if it exists if (document.getElementById('list-group-' + userId)) { document.getElementById('list-group-' + userId).remove(); } @@ -16,13 +26,16 @@ export default function upsertPlayerItem (player, USER_ID) { const playerItem = document.createElement('a'); playerItem.className = `list-group-item ${userId === USER_ID ? 'user-score' : ''} clickable`; playerItem.id = `list-group-${userId}`; + const displayUsername = (playerIsOwner && !isPublic) ? `👑 ${escapeHTML(username)}` : escapeHTML(username); + playerItem.innerHTML = `
- ${escapeHTML(username)} + ${displayUsername} ${points}
- `; + `; + // Set attributes for the popover playerItem.setAttribute('data-bs-container', 'body'); playerItem.setAttribute('data-bs-custom-class', 'custom-popover'); playerItem.setAttribute('data-bs-html', 'true'); @@ -31,33 +44,69 @@ export default function upsertPlayerItem (player, USER_ID) { playerItem.setAttribute('data-bs-trigger', 'focus'); playerItem.setAttribute('tabindex', '0'); + // Popover content playerItem.setAttribute('data-bs-title', username); playerItem.setAttribute('data-bs-content', ` -
    -
  • - Powers - ${powers} -
  • -
  • - Tens - ${tens} -
  • -
  • - Negs - ${negs} -
  • -
  • - TUH - ${tuh} -
  • -
  • - Celerity - ${celerity.toFixed(3)} -
  • -
- `); +
    +
  • Powers${powers}
  • +
  • Tens${tens}
  • +
  • Negs${negs}
  • +
  • TUH${tuh}
  • +
  • Celerity${celerity.toFixed(3)}
  • +
  • Is Owner?${playerIsOwner ? 'Yes' : 'No'}
  • +
+ `); document.getElementById('player-list-group').appendChild(playerItem); + + // ban button if the viewer is the owner and the player is not, also room has to be private + if ((ownerId === USER_ID) && userId !== ownerId && !isPublic && userId !== 'ai-bot') { + const banButton = document.createElement('button'); + banButton.className = 'btn btn-danger btn-sm mt-2 me-1'; + banButton.title = 'Ban an user. They can no longer join the room.'; + banButton.innerText = 'Ban'; + playerItem.appendChild(banButton); + banButton.addEventListener('click', () => { + socket.send(JSON.stringify({ type: 'ban', targetId: userId, targetUsername: username })); + }); + } + + // votekick button. cannot vk an owner (change? idk) + if (userId !== USER_ID && (isPublic || (userId !== ownerId && userId !== 'ai-bot'))) { + const vkButton = document.createElement('button'); + vkButton.className = 'btn btn-warning btn-sm mt-2 me-1'; + vkButton.title = 'Initiate a votekick on an user. 90 second cooldown.'; + vkButton.innerText = 'VK'; + playerItem.appendChild(vkButton); + vkButton.addEventListener('click', () => { + socket.send(JSON.stringify({ type: 'votekick-vote', targetId: userId })); + socket.send(JSON.stringify({ type: 'votekick-init', targetId: userId })); + vkButton.disabled = true; + vkButton.innerText = 'Cooldown'; + setTimeout(() => { + vkButton.disabled = false; + vkButton.innerText = 'VK'; + }, 90000); + }); + } + // User cannot be ai or yourself + if (userId !== 'ai-bot' && userId !== USER_ID && !isPublic) { + const muteButton = document.createElement('button'); + + muteButton.className = 'btn btn-warning btn-sm mt-2 me-1'; + muteButton.title = 'Mute/Unmute an user to change visibility of what they say in chat.'; + muteButton.innerText = 'Mute'; + playerItem.appendChild(muteButton); + muteButton.addEventListener('click', () => { + socket.send(JSON.stringify({ type: 'toggle-mute', targetId: userId, muteStatus: muteButton.innerText })); + if (muteButton.innerText === 'Unmute') { + muteButton.innerText = 'Mute'; + } else { + muteButton.innerText = 'Unmute'; + } + }); + } + // bootstrap requires "new" to be called on each popover // eslint-disable-next-line no-new new bootstrap.Popover(playerItem); diff --git a/quizbowl/Player.js b/quizbowl/Player.js index f4647ddf7..e0ece4d81 100644 --- a/quizbowl/Player.js +++ b/quizbowl/Player.js @@ -4,7 +4,6 @@ class Player { this.MAX_USERNAME_LENGTH = MAX_USERNAME_LENGTH; this.username = ''; - this.powers = 0; this.tens = 0; this.zeroes = 0; diff --git a/quizbowl/TossupRoom.js b/quizbowl/TossupRoom.js index 2887969d7..d3c1029ee 100644 --- a/quizbowl/TossupRoom.js +++ b/quizbowl/TossupRoom.js @@ -85,12 +85,9 @@ export default class TossupRoom extends Room { case 'buzz': return this.buzz(userId, message); case 'clear-stats': return this.clearStats(userId, message); case 'give-answer': return this.giveAnswer(userId, message); - case 'next': case 'skip': - case 'start': - return this.next(userId, message); - + case 'start': return this.next(userId, message); case 'pause': return this.pause(userId, message); case 'set-categories': return this.setCategories(userId, message); case 'set-difficulties': return this.setDifficulties(userId, message); @@ -411,7 +408,7 @@ export default class TossupRoom extends Room { // calculate time needed before reading next word let time = Math.log(word.length) + 1; if ((word.endsWith('.') && word.charCodeAt(word.length - 2) > 96 && word.charCodeAt(word.length - 2) < 123) || - word.slice(-2) === '.\u201d' || word.slice(-2) === '!\u201d' || word.slice(-2) === '?\u201d') { + word.slice(-2) === '.\u201d' || word.slice(-2) === '!\u201d' || word.slice(-2) === '?\u201d') { time += 2; } else if (word.endsWith(',') || word.slice(-2) === ',\u201d') { time += 0.75; diff --git a/routes/api/query.js b/routes/api/query.js index c41de3c33..7b3df20bc 100644 --- a/routes/api/query.js +++ b/routes/api/query.js @@ -67,7 +67,7 @@ router.get('/', async (req, res) => { try { const queryResult = await getQuery(req.query); - res.json(queryResult); + res.json(queryResult); } catch (error) { switch (error.message) { case 'Invalid question type specified.': diff --git a/server/multiplayer/ServerTossupRoom.js b/server/multiplayer/ServerTossupRoom.js index 1f7b769ce..9dcb8c05c 100644 --- a/server/multiplayer/ServerTossupRoom.js +++ b/server/multiplayer/ServerTossupRoom.js @@ -1,4 +1,5 @@ import ServerPlayer from './ServerPlayer.js'; +import Votekick from './VoteKick.js'; import { HEADER, ENDC, OKBLUE, OKGREEN } from '../bcolors.js'; import isAppropriateString from '../moderation/is-appropriate-string.js'; import insertTokensIntoHTML from '../../quizbowl/insert-tokens-into-html.js'; @@ -13,14 +14,19 @@ import getNumPackets from '../../database/qbreader/get-num-packets.js'; import checkAnswer from 'qb-answer-checker'; export default class ServerTossupRoom extends TossupRoom { - constructor (name, isPermanent = false, categories = [], subcategories = [], alternateSubcategories = []) { + constructor (name, ownerId, isPermanent = false, categories = [], subcategories = [], alternateSubcategories = []) { super(name, categories, subcategories, alternateSubcategories); + this.ownerId = ownerId; this.isPermanent = isPermanent; this.checkAnswer = checkAnswer; this.getNumPackets = getNumPackets; this.getRandomTossups = getRandomTossups; this.getSet = getSet; this.getSetList = getSetList; + this.bannedUserList = []; + this.kickedUserList = []; + this.votekickList = []; + this.lastVotekickTime = {}; this.rateLimiter = new RateLimit(50, 1000); this.rateLimitExceeded = new Set(); @@ -36,18 +42,31 @@ export default class ServerTossupRoom extends TossupRoom { async message (userId, message) { switch (message.type) { + case 'ban': return this.ban(userId, message); case 'chat': return this.chat(userId, message); case 'chat-live-update': return this.chatLiveUpdate(userId, message); case 'give-answer-live-update': return this.giveAnswerLiveUpdate(userId, message); case 'toggle-lock': return this.toggleLock(userId, message); case 'toggle-login-required': return this.toggleLoginRequired(userId, message); + case 'toggle-mute': return this.toggleMute(userId, message); case 'toggle-public': return this.togglePublic(userId, message); + case 'votekick-init': return this.votekickInit(userId, message); + case 'votekick-vote': return this.votekickVote(userId, message); default: super.message(userId, message); } } + ban (userId, { targetId, targetUsername }) { + console.log('Ban request recieved. Target ' + targetId); + if (this.ownerId !== userId) { return; } + + console.log('Checked, owner sent ban'); + this.emitMessage({ type: 'confirm-ban', targetId, targetUsername }); + this.bannedUserList.push(targetId); + } + connection (socket, userId, username) { - 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}`); + console.log(`Connection in room ${HEADER}${this.name}${ENDC} - ID of owner: ${OKBLUE}${this.ownerId}${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}`); const isNew = !(userId in this.players); if (isNew) { this.players[userId] = new ServerPlayer(userId); } @@ -55,6 +74,18 @@ export default class ServerTossupRoom extends TossupRoom { this.sockets[userId] = socket; username = this.players[userId].safelySetUsername(username); + if (this.bannedUserList.includes(userId)) { + console.log(`Banned user ${userId} (${username}) tried to join a room`); + this.sendToSocket(userId, { type: 'enforcing-removal', removalType: 'ban' }); + return; + } + + if (this.kickedUserList.includes(userId)) { + console.log(`Kicked user ${userId} (${username}) tried to join a room`); + this.sendToSocket(userId, { type: 'enforcing-removal', removalType: 'kick' }); + return; + } + socket.on('message', message => { if (this.rateLimiter(socket) && !this.rateLimitExceeded.has(username)) { console.log(`Rate limit exceeded for ${username} in room ${this.name}`); @@ -77,6 +108,7 @@ export default class ServerTossupRoom extends TossupRoom { type: 'connection-acknowledged', userId, + ownerId: this.ownerId, players: this.players, isPermanent: this.isPermanent, @@ -191,6 +223,10 @@ export default class ServerTossupRoom extends TossupRoom { this.emitMessage({ type: 'toggle-login-required', loginRequired, username }); } + toggleMute (userId, { targetId, muteStatus }) { + this.sendToSocket(userId, { type: 'mute-player', targetId, muteStatus }); + } + togglePublic (userId, { public: isPublic }) { if (this.isPermanent) { return; } this.settings.public = isPublic; @@ -215,4 +251,51 @@ export default class ServerTossupRoom extends TossupRoom { if (this.settings.public) { return; } super.toggleTimer(userId, { timer }); } + + votekickInit (userId, { targetId }) { + const targetUsername = this.players[targetId].username; + + const currentTime = Date.now(); + if (this.lastVotekickTime[userId] && (currentTime - this.lastVotekickTime[userId] < 90000)) { + console.log(`Votekick denied: ${userId} 90 second cooldown violation.`); + return; + } + + this.lastVotekickTime[userId] = currentTime; + + for (const votekick of this.votekickList) { + if (votekick.exists(targetId)) { return; } + } + + const threshold = Math.max((Object.keys(this.players).length) - 1, 0); + const votekick = new Votekick(targetId, threshold, []); + votekick.vote(userId); + if (votekick.check()) { + this.emitMessage({ type: 'successful-vk', targetUsername, targetId }); + this.kickedUserList.push(targetId); + } else { + this.votekickList.push(votekick); + this.emitMessage({ type: 'initiated-vk', targetUsername, threshold }); + } + } + + votekickVote (userId, { targetId }) { + const targetUsername = this.players[targetId].username; + + let exists = false; + let thisVotekick; + for (const votekick of this.votekickList) { + if (votekick.exists(targetId)) { + thisVotekick = votekick; + exists = true; + } + } + if (!exists) { return; } + + thisVotekick.vote(userId); + if (thisVotekick.check()) { + this.emitMessage({ type: 'successful-vk', targetUsername, targetId }); + this.kickedUserList.push(targetId); + } + } } diff --git a/server/multiplayer/VoteKick.js b/server/multiplayer/VoteKick.js new file mode 100644 index 000000000..5c1f90d77 --- /dev/null +++ b/server/multiplayer/VoteKick.js @@ -0,0 +1,21 @@ +export default class Votekick { + constructor (targetId, threshold, voted = []) { + this.targetId = targetId; + this.voted = Array.isArray(voted) ? voted : []; + this.threshold = threshold; + } + + exists (givenId) { + return this.targetId === givenId; + } + + vote (votingId) { + if (!this.voted.includes(votingId)) { + this.voted.push(votingId); + } + } + + check () { + return this.voted.length >= this.threshold; + } +} diff --git a/server/multiplayer/handle-wss-connection.js b/server/multiplayer/handle-wss-connection.js index 304a59a17..4bca13a7c 100644 --- a/server/multiplayer/handle-wss-connection.js +++ b/server/multiplayer/handle-wss-connection.js @@ -16,7 +16,7 @@ const DOMPurify = createDOMPurify(window); const tossupRooms = {}; for (const room of PERMANENT_ROOMS) { const { name, categories, subcategories } = room; - tossupRooms[name] = new ServerTossupRoom(name, true, categories, subcategories); + tossupRooms[name] = new ServerTossupRoom(name, 0, true, categories, subcategories); } /** @@ -25,12 +25,12 @@ for (const room of PERMANENT_ROOMS) { * @param {String} roomName * @returns {TossupRoom} */ -function createAndReturnRoom (roomName, isPrivate = false) { +function createAndReturnRoom (roomName, userId, isPrivate = false) { roomName = DOMPurify.sanitize(roomName); roomName = roomName?.substring(0, ROOM_NAME_MAX_LENGTH) ?? ''; if (!Object.prototype.hasOwnProperty.call(tossupRooms, roomName)) { - const newRoom = new ServerTossupRoom(roomName, false); + const newRoom = new ServerTossupRoom(roomName, userId, false); newRoom.settings.public = !isPrivate; tossupRooms[roomName] = newRoom; } @@ -68,7 +68,7 @@ export default function handleWssConnection (ws, req) { return false; } - const room = createAndReturnRoom(roomName, isPrivate); + const room = createAndReturnRoom(roomName, userId, isPrivate); if (room.settings.lock === true) { ws.send(JSON.stringify({ type: 'error',