Fix: voting flow bugs na code review

- acceptingMatch/finalizingMatches als const gedeclareerd (ipv implicit global)
- 30-min voting timeout met cleanup interval
- _created timestamp bij acceptingMatch init
- finalizingMatches semaphore tegen race conditions
- try/catch + logError in finalizeMatch()
- Speler check in vote callbacks (m.players.includes)
- finalizingMatches.delete bij cleanup na finalize
This commit is contained in:
Nova Coder
2026-05-25 18:46:13 +00:00
parent 4f700368e3
commit 874618c328
+35 -3
View File
@@ -155,6 +155,21 @@ function setUserState(chatId, data) {
userState.set(chatId, data); userState.set(chatId, data);
} }
// ─── Match voting state (per match pending ranked/friendly vote) ───
const acceptingMatch = {};
const finalizingMatches = new Set(); // semaphore tegen race condition
// Voting timeout: clean up matches older than 30 minutes
setInterval(() => {
const now = Date.now();
for (const [matchId, m] of Object.entries(acceptingMatch)) {
if (m._created && now - m._created > 30 * 60 * 1000) {
delete acceptingMatch[matchId];
log('Voting timeout: match ' + matchId + ' cleaned up');
}
}
}, 60 * 1000); // check elke minuut
// ─── Safe sendMessage wrapper (met rate limiting) ─── // ─── Safe sendMessage wrapper (met rate limiting) ───
const sendMsg = throttledSend; const sendMsg = throttledSend;
@@ -1549,7 +1564,8 @@ bot.on('callback_query', async (query) => {
location: matchInfo.location, location: matchInfo.location,
proposed_teams: matchInfo.proposed_teams, proposed_teams: matchInfo.proposed_teams,
ranked: new Set(), ranked: new Set(),
friendly: new Set() friendly: new Set(),
_created: Date.now()
}; };
bot.answerCallbackQuery(query.id, { text: 'Aanwezig!' }).catch(() => {}); bot.answerCallbackQuery(query.id, { text: 'Aanwezig!' }).catch(() => {});
@@ -1593,6 +1609,12 @@ bot.on('callback_query', async (query) => {
const player = players.find(p => p.telegram_id == chatId); const player = players.find(p => p.telegram_id == chatId);
if (!player) return; if (!player) return;
// Security: only match players can vote
if (!m.players.includes(player.id)) {
bot.answerCallbackQuery(query.id, { text: 'Je zit niet in deze match' }).catch(() => {});
return;
}
if (m.ranked.has(player.id) || m.friendly.has(player.id)) { if (m.ranked.has(player.id) || m.friendly.has(player.id)) {
bot.answerCallbackQuery(query.id, { text: 'Je hebt al gestemd!' }).catch(() => {}); bot.answerCallbackQuery(query.id, { text: 'Je hebt al gestemd!' }).catch(() => {});
return; return;
@@ -1637,11 +1659,20 @@ bot.on('callback_query', async (query) => {
// ─── Finaliseer match ─── // ─── Finaliseer match ───
async function finalizeMatch(matchId, finalType) { async function finalizeMatch(matchId, finalType) {
if (finalizingMatches.has(matchId)) return; // prevent race
finalizingMatches.add(matchId);
const players = await api('/players'); const players = await api('/players');
const m = acceptingMatch[matchId]; const m = acceptingMatch[matchId];
if (!m) return; if (!m) { finalizingMatches.delete(matchId); return; }
try { await api('/matches/' + matchId + '/type', 'POST', { match_type: finalType }); } catch(e) {} try {
await api('/matches/' + matchId + '/type', 'POST', { match_type: finalType });
} catch(e) {
logError('finalizeMatch: failed to set match type:', e.message);
finalizingMatches.delete(matchId);
return;
}
const getName = id => { const p = players.find(x => x.id == id); return p ? p.name : '?'; }; const getName = id => { const p = players.find(x => x.id == id); return p ? p.name : '?'; };
const t1names = (m.proposed_teams?.team1 || []).map(getName).join(' + '); const t1names = (m.proposed_teams?.team1 || []).map(getName).join(' + ');
@@ -1659,6 +1690,7 @@ bot.on('callback_query', async (query) => {
} }
delete acceptingMatch[matchId]; delete acceptingMatch[matchId];
finalizingMatches.delete(matchId);
log('Match ' + matchId + ' finalized as ' + finalType); log('Match ' + matchId + ' finalized as ' + finalType);
} }
} else if (data.startsWith('team_swap_')) { } else if (data.startsWith('team_swap_')) {