Files
jackboxpartypack-gamepicker/backend/routes/sessions.js

578 lines
16 KiB
JavaScript

const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const db = require('../database');
const router = express.Router();
// Get all sessions
router.get('/', (req, res) => {
try {
const sessions = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
GROUP BY s.id
ORDER BY s.created_at DESC
`).all();
res.json(sessions);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get active session
router.get('/active', (req, res) => {
try {
const session = 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.is_active = 1
GROUP BY s.id
LIMIT 1
`).get();
// Return null instead of 404 when no active session
if (!session) {
return res.json({ session: null, message: 'No active session' });
}
res.json(session);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get single session by ID
router.get('/:id', (req, res) => {
try {
const session = 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(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
res.json(session);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new session (admin only)
router.post('/', authenticateToken, (req, res) => {
try {
const { notes } = req.body;
// Check if there's already an active session
const activeSession = db.prepare('SELECT id FROM sessions WHERE is_active = 1').get();
if (activeSession) {
return res.status(400).json({
error: 'An active session already exists. Please close it before creating a new one.',
activeSessionId: activeSession.id
});
}
const stmt = db.prepare(`
INSERT INTO sessions (notes, is_active)
VALUES (?, 1)
`);
const result = stmt.run(notes || null);
const newSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(newSession);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Close/finalize session (admin only)
router.post('/:id/close', authenticateToken, (req, res) => {
try {
const { notes } = req.body;
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.is_active === 0) {
return res.status(400).json({ error: 'Session is already closed' });
}
// Set all 'playing' games to 'played' before closing
db.prepare(`
UPDATE session_games
SET status = 'played'
WHERE session_id = ? AND status = 'playing'
`).run(req.params.id);
const stmt = db.prepare(`
UPDATE sessions
SET is_active = 0, closed_at = CURRENT_TIMESTAMP, notes = COALESCE(?, notes)
WHERE id = ?
`);
stmt.run(notes || null, req.params.id);
const closedSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
res.json(closedSession);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete session (admin only)
router.delete('/:id', authenticateToken, (req, res) => {
try {
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Prevent deletion of active sessions
if (session.is_active === 1) {
return res.status(400).json({ error: 'Cannot delete an active session. Please close it first.' });
}
// Delete related data first (cascade)
db.prepare('DELETE FROM chat_logs WHERE session_id = ?').run(req.params.id);
db.prepare('DELETE FROM session_games WHERE session_id = ?').run(req.params.id);
// Delete the session
db.prepare('DELETE FROM sessions WHERE id = ?').run(req.params.id);
res.json({ message: 'Session deleted successfully', sessionId: parseInt(req.params.id) });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get games played in a session
router.get('/:id/games', (req, res) => {
try {
const games = db.prepare(`
SELECT
sg.*,
g.pack_name,
g.title,
g.game_type,
g.min_players,
g.max_players,
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(req.params.id);
res.json(games);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Add game to session (admin only)
router.post('/:id/games', authenticateToken, (req, res) => {
try {
const { game_id, manually_added } = req.body;
if (!game_id) {
return res.status(400).json({ error: 'game_id is required' });
}
// Verify session exists and is active
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.is_active === 0) {
return res.status(400).json({ error: 'Cannot add games to a closed session' });
}
// Verify game exists
const game = db.prepare('SELECT * FROM games WHERE id = ?').get(game_id);
if (!game) {
return res.status(404).json({ error: 'Game not found' });
}
// Set all current 'playing' games to 'played' (except skipped ones)
db.prepare(`
UPDATE session_games
SET status = CASE
WHEN status = 'skipped' THEN 'skipped'
ELSE 'played'
END
WHERE session_id = ? AND status = 'playing'
`).run(req.params.id);
// Add game to session with 'playing' status
const stmt = db.prepare(`
INSERT INTO session_games (session_id, game_id, manually_added, status)
VALUES (?, ?, ?, 'playing')
`);
const result = stmt.run(req.params.id, game_id, manually_added ? 1 : 0);
// Increment play count for the game
db.prepare('UPDATE games SET play_count = play_count + 1 WHERE id = ?').run(game_id);
const sessionGame = db.prepare(`
SELECT
sg.*,
g.pack_name,
g.title,
g.game_type
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.id = ?
`).get(result.lastInsertRowid);
res.status(201).json(sessionGame);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Import chat log and process popularity (admin only)
router.post('/:id/chat-import', authenticateToken, (req, res) => {
try {
const { chatData } = req.body;
if (!chatData || !Array.isArray(chatData)) {
return res.status(400).json({ error: 'chatData must be an array' });
}
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Get all games played in this session with timestamps
const sessionGames = db.prepare(`
SELECT sg.game_id, sg.played_at, g.title
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ?
ORDER BY sg.played_at ASC
`).all(req.params.id);
if (sessionGames.length === 0) {
return res.status(400).json({ error: 'No games played in this session to match votes against' });
}
let votesProcessed = 0;
const votesByGame = {};
const insertChatLog = db.prepare(`
INSERT INTO chat_logs (session_id, chatter_name, message, timestamp, parsed_vote)
VALUES (?, ?, ?, ?, ?)
`);
const updatePopularity = db.prepare(`
UPDATE games SET popularity_score = popularity_score + ? WHERE id = ?
`);
const processVotes = db.transaction((messages) => {
for (const msg of messages) {
const { username, message, timestamp } = msg;
if (!username || !message || !timestamp) {
continue;
}
// Check for vote patterns
let vote = null;
if (message.includes('thisgame++')) {
vote = 'thisgame++';
} else if (message.includes('thisgame--')) {
vote = 'thisgame--';
}
// Insert into chat logs
insertChatLog.run(
req.params.id,
username,
message,
timestamp,
vote
);
if (vote) {
// Find which game was being played at this timestamp
const messageTime = new Date(timestamp).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 (messageTime >= currentGameTime && messageTime < nextGameTime) {
matchedGame = currentGame;
break;
}
} else {
// Last game in session
if (messageTime >= currentGameTime) {
matchedGame = currentGame;
break;
}
}
}
if (matchedGame) {
const points = vote === 'thisgame++' ? 1 : -1;
updatePopularity.run(points, matchedGame.game_id);
if (!votesByGame[matchedGame.game_id]) {
votesByGame[matchedGame.game_id] = {
title: matchedGame.title,
upvotes: 0,
downvotes: 0
};
}
if (points > 0) {
votesByGame[matchedGame.game_id].upvotes++;
} else {
votesByGame[matchedGame.game_id].downvotes++;
}
votesProcessed++;
}
}
}
});
processVotes(chatData);
res.json({
message: 'Chat log imported and processed successfully',
messagesImported: chatData.length,
votesProcessed,
votesByGame
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update session game status (admin only)
router.patch('/:sessionId/games/:gameId/status', authenticateToken, (req, res) => {
try {
const { status } = req.body;
const { sessionId, gameId } = req.params;
if (!status || !['playing', 'played', 'skipped'].includes(status)) {
return res.status(400).json({ error: 'Invalid status. Must be playing, played, or skipped' });
}
// If setting to 'playing', first set all other games in session to 'played' or keep as 'skipped'
if (status === 'playing') {
db.prepare(`
UPDATE session_games
SET status = CASE
WHEN status = 'skipped' THEN 'skipped'
ELSE 'played'
END
WHERE session_id = ? AND status = 'playing'
`).run(sessionId);
}
// Update the specific game
const result = db.prepare(`
UPDATE session_games
SET status = ?
WHERE session_id = ? AND id = ?
`).run(status, sessionId, gameId);
if (result.changes === 0) {
return res.status(404).json({ error: 'Session game not found' });
}
res.json({ message: 'Status updated successfully', status });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete session game (admin only)
router.delete('/:sessionId/games/:gameId', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
const result = db.prepare(`
DELETE FROM session_games
WHERE session_id = ? AND id = ?
`).run(sessionId, gameId);
if (result.changes === 0) {
return res.status(404).json({ error: 'Session game not found' });
}
res.json({ message: 'Game removed from session successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Export session data (plaintext and JSON)
router.get('/:id/export', authenticateToken, (req, res) => {
try {
const { format } = req.query; // 'json' or 'txt'
const sessionId = req.params.id;
// Get session info
const session = 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(sessionId);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Get games for this session
const games = db.prepare(`
SELECT
sg.*,
g.title,
g.pack_name,
g.min_players,
g.max_players,
g.game_type
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ?
ORDER BY sg.played_at ASC
`).all(sessionId);
// Get chat logs if any
const chatLogs = db.prepare(`
SELECT * FROM chat_logs
WHERE session_id = ?
ORDER BY timestamp ASC
`).all(sessionId);
if (format === 'json') {
// JSON Export
const exportData = {
session: {
id: session.id,
created_at: session.created_at,
closed_at: session.closed_at,
is_active: session.is_active === 1,
notes: session.notes,
games_played: session.games_played
},
games: games.map(game => ({
title: game.title,
pack: game.pack_name,
players: `${game.min_players}-${game.max_players}`,
type: game.game_type,
played_at: game.played_at,
manually_added: game.manually_added === 1,
status: game.status
})),
chat_logs: chatLogs.map(log => ({
username: log.chatter_name,
message: log.message,
timestamp: log.timestamp,
vote: log.parsed_vote
}))
};
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.json"`);
res.send(JSON.stringify(exportData, null, 2));
} else {
// Plain Text Export
let text = `JACKBOX GAME PICKER - SESSION EXPORT\n`;
text += `${'='.repeat(50)}\n\n`;
text += `Session ID: ${session.id}\n`;
text += `Created: ${session.created_at}\n`;
if (session.closed_at) {
text += `Closed: ${session.closed_at}\n`;
}
text += `Status: ${session.is_active ? 'Active' : 'Ended'}\n`;
if (session.notes) {
text += `Notes: ${session.notes}\n`;
}
text += `\nGames Played: ${session.games_played}\n`;
text += `\n${'='.repeat(50)}\n\n`;
if (games.length > 0) {
text += `GAMES:\n`;
text += `${'-'.repeat(50)}\n`;
games.forEach((game, index) => {
text += `\n${index + 1}. ${game.title}\n`;
text += ` Pack: ${game.pack_name}\n`;
text += ` Players: ${game.min_players}-${game.max_players}\n`;
if (game.game_type) {
text += ` Type: ${game.game_type}\n`;
}
text += ` Played: ${game.played_at}\n`;
text += ` Status: ${game.status}\n`;
if (game.manually_added === 1) {
text += ` (Manually Added)\n`;
}
});
text += `\n${'-'.repeat(50)}\n`;
}
if (chatLogs.length > 0) {
text += `\nCHAT LOGS:\n`;
text += `${'-'.repeat(50)}\n`;
chatLogs.forEach(log => {
text += `\n[${log.timestamp}] ${log.chatter_name}:\n`;
text += ` ${log.message}\n`;
if (log.parsed_vote) {
text += ` Vote: ${log.parsed_vote}\n`;
}
});
text += `\n${'-'.repeat(50)}\n`;
}
text += `\nEnd of Session Export\n`;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.txt"`);
res.send(text);
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;