diff --git a/bot.js b/bot.js index 444627d..25ece7b 100644 --- a/bot.js +++ b/bot.js @@ -155,6 +155,21 @@ function setUserState(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) ─── const sendMsg = throttledSend; @@ -1549,7 +1564,8 @@ bot.on('callback_query', async (query) => { location: matchInfo.location, proposed_teams: matchInfo.proposed_teams, ranked: new Set(), - friendly: new Set() + friendly: new Set(), + _created: Date.now() }; 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); 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)) { bot.answerCallbackQuery(query.id, { text: 'Je hebt al gestemd!' }).catch(() => {}); return; @@ -1637,11 +1659,20 @@ bot.on('callback_query', async (query) => { // ─── Finaliseer match ─── async function finalizeMatch(matchId, finalType) { + if (finalizingMatches.has(matchId)) return; // prevent race + finalizingMatches.add(matchId); + const players = await api('/players'); 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 t1names = (m.proposed_teams?.team1 || []).map(getName).join(' + '); @@ -1659,6 +1690,7 @@ bot.on('callback_query', async (query) => { } delete acceptingMatch[matchId]; + finalizingMatches.delete(matchId); log('Match ' + matchId + ' finalized as ' + finalType); } } else if (data.startsWith('team_swap_')) {