const express = require('express'); const { authenticateToken } = require('../middleware/auth'); const db = require('../database'); const router = express.Router(); // Get vote history with filtering and pagination router.get('/', (req, res) => { try { let { session_id, game_id, username, vote_type, page, limit } = req.query; page = parseInt(page) || 1; limit = Math.min(parseInt(limit) || 50, 100); if (page < 1) page = 1; if (limit < 1) limit = 50; const offset = (page - 1) * limit; const where = []; const params = []; if (session_id !== undefined) { const sid = parseInt(session_id); if (isNaN(sid)) { return res.status(400).json({ error: 'session_id must be an integer' }); } where.push('lv.session_id = ?'); params.push(sid); } if (game_id !== undefined) { const gid = parseInt(game_id); if (isNaN(gid)) { return res.status(400).json({ error: 'game_id must be an integer' }); } where.push('lv.game_id = ?'); params.push(gid); } if (username) { where.push('lv.username = ?'); params.push(username); } if (vote_type !== undefined) { if (vote_type !== 'up' && vote_type !== 'down') { return res.status(400).json({ error: 'vote_type must be "up" or "down"' }); } where.push('lv.vote_type = ?'); params.push(vote_type === 'up' ? 1 : -1); } const whereClause = where.length > 0 ? 'WHERE ' + where.join(' AND ') : ''; const countResult = db.prepare( `SELECT COUNT(*) as total FROM live_votes lv ${whereClause}` ).get(...params); const total = countResult.total; const total_pages = Math.ceil(total / limit) || 0; const votes = db.prepare(` SELECT lv.id, lv.session_id, lv.game_id, g.title AS game_title, g.pack_name, lv.username, CASE WHEN lv.vote_type = 1 THEN 'up' ELSE 'down' END AS vote_type, lv.timestamp, lv.created_at FROM live_votes lv JOIN games g ON lv.game_id = g.id ${whereClause} ORDER BY lv.timestamp DESC LIMIT ? OFFSET ? `).all(...params, limit, offset); res.json({ votes, pagination: { page, limit, total, total_pages }, }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Live vote endpoint - receives real-time votes from bot router.post('/live', authenticateToken, (req, res) => { try { const { username, vote, timestamp } = req.body; // Validate payload if (!username || !vote || !timestamp) { return res.status(400).json({ error: 'Missing required fields: username, vote, timestamp' }); } if (vote !== 'up' && vote !== 'down') { return res.status(400).json({ error: 'vote must be either "up" or "down"' }); } // Validate timestamp format const voteTimestamp = new Date(timestamp); if (isNaN(voteTimestamp.getTime())) { return res.status(400).json({ error: 'Invalid timestamp format. Use ISO 8601 format (e.g., 2025-11-01T20:30:00Z)' }); } // Check for active session const activeSession = db.prepare(` SELECT * FROM sessions WHERE is_active = 1 LIMIT 1 `).get(); if (!activeSession) { return res.status(404).json({ error: 'No active session found' }); } // Get all games played in this session with timestamps const sessionGames = db.prepare(` SELECT sg.game_id, sg.played_at, g.title, g.upvotes, g.downvotes, g.popularity_score FROM session_games sg JOIN games g ON sg.game_id = g.id WHERE sg.session_id = ? ORDER BY sg.played_at ASC `).all(activeSession.id); if (sessionGames.length === 0) { return res.status(404).json({ error: 'No games have been played in the active session yet' }); } // Match vote timestamp to the correct game using interval logic const voteTime = voteTimestamp.getTime(); let matchedGame = null; for (let i = 0; i < sessionGames.length; i++) { const currentGame = sessionGames[i]; const nextGame = sessionGames[i + 1]; const currentGameTime = new Date(currentGame.played_at).getTime(); if (nextGame) { const nextGameTime = new Date(nextGame.played_at).getTime(); if (voteTime >= currentGameTime && voteTime < nextGameTime) { matchedGame = currentGame; break; } } else { // Last game in session - vote belongs here if timestamp is after this game started if (voteTime >= currentGameTime) { matchedGame = currentGame; break; } } } if (!matchedGame) { return res.status(404).json({ error: 'Vote timestamp does not match any game in the active session', debug: { voteTimestamp: timestamp, sessionGames: sessionGames.map(g => ({ title: g.title, played_at: g.played_at })) } }); } // Check for duplicate vote (within 1 second window) // Get the most recent vote from this user const lastVote = db.prepare(` SELECT timestamp FROM live_votes WHERE username = ? ORDER BY created_at DESC LIMIT 1 `).get(username); if (lastVote) { const lastVoteTime = new Date(lastVote.timestamp).getTime(); const currentVoteTime = new Date(timestamp).getTime(); const timeDiffSeconds = Math.abs(currentVoteTime - lastVoteTime) / 1000; if (timeDiffSeconds <= 1) { return res.status(409).json({ error: 'Duplicate vote detected (within 1 second of previous vote)', message: 'Please wait at least 1 second between votes', timeSinceLastVote: timeDiffSeconds }); } } // Process the vote in a transaction const voteType = vote === 'up' ? 1 : -1; const insertVote = db.prepare(` INSERT INTO live_votes (session_id, game_id, username, vote_type, timestamp) VALUES (?, ?, ?, ?, ?) `); const updateUpvote = db.prepare(` UPDATE games SET upvotes = upvotes + 1, popularity_score = popularity_score + 1 WHERE id = ? `); const updateDownvote = db.prepare(` UPDATE games SET downvotes = downvotes + 1, popularity_score = popularity_score - 1 WHERE id = ? `); const processVote = db.transaction(() => { insertVote.run(activeSession.id, matchedGame.game_id, username, voteType, timestamp); if (voteType === 1) { updateUpvote.run(matchedGame.game_id); } else { updateDownvote.run(matchedGame.game_id); } }); processVote(); // Get updated game stats const updatedGame = db.prepare(` SELECT id, title, upvotes, downvotes, popularity_score FROM games WHERE id = ? `).get(matchedGame.game_id); // Get session stats const sessionStats = db.prepare(` SELECT s.*, COUNT(sg.id) as games_played FROM sessions s LEFT JOIN session_games sg ON s.id = sg.session_id WHERE s.id = ? GROUP BY s.id `).get(activeSession.id); res.json({ success: true, message: 'Vote recorded successfully', session: { id: sessionStats.id, games_played: sessionStats.games_played }, game: { id: updatedGame.id, title: updatedGame.title, upvotes: updatedGame.upvotes, downvotes: updatedGame.downvotes, popularity_score: updatedGame.popularity_score }, vote: { username: username, type: vote, timestamp: timestamp } }); } catch (error) { console.error('Error processing live vote:', error); res.status(500).json({ error: error.message }); } }); module.exports = router;