diff --git a/html/admin.html b/html/admin.html index 6ce39dc..4aef34e 100644 --- a/html/admin.html +++ b/html/admin.html @@ -284,6 +284,7 @@ + @@ -406,6 +407,40 @@ + + +
+
+
+

Chat Moderation

+
+ + +
+
+

+ Delete offensive messages (they'll be removed from all clients). Ban users from chat if needed. +

+ + + + + + + + + + + + + + + +
IDTimePlayerMessageUser IDStatusActions
Loading...
+
+
@@ -500,6 +535,7 @@ else if (tab === 'payouts') loadPayouts(); else if (tab === 'games') loadGames(); else if (tab === 'users') loadUsers(); + else if (tab === 'chat') loadChat(); } async function loadDashboard() { @@ -806,6 +842,142 @@ loadUsers(0); } + // Chat moderation functions + async function loadChat() { + const includeDeleted = document.getElementById('showDeletedMessages')?.checked || false; + + try { + const url = `/api/admin/chat?limit=100&includeDeleted=${includeDeleted}`; + const res = await fetch(url, { headers: { 'X-Admin-Key': apiKey } }); + const data = await res.json(); + + const tbody = document.getElementById('chatTable'); + if (!data.messages?.length) { + tbody.innerHTML = 'No chat messages'; + return; + } + + tbody.innerHTML = data.messages.map(m => { + const isDeleted = !!m.deletedAt; + const isBanned = m.chatBanned; + const rowStyle = isDeleted ? 'opacity: 0.5; text-decoration: line-through;' : ''; + + return ` + + ${m.id} + ${formatTimeAgo(new Date(m.timestamp))} + ${m.playerId || m.username || '-'} + ${escapeHtml(m.message)} + ${m.userId || '-'} + + ${isDeleted ? 'Deleted' : ''} + ${isBanned ? 'Banned' : ''} + ${!isDeleted && !isBanned ? 'Active' : ''} + + + ${!isDeleted ? `` : ''} + ${m.userId && !isBanned ? `` : ''} + ${m.userId && isBanned ? `` : ''} + + + `; + }).join(''); + } catch (e) { + console.error('Failed to load chat:', e); + document.getElementById('chatTable').innerHTML = 'Error loading chat'; + } + } + + async function deleteMessage(messageId) { + if (!confirm('Delete this message? It will be removed from all clients.')) return; + + try { + const res = await fetch(`/api/admin/chat/${messageId}`, { + method: 'DELETE', + headers: { + 'X-Admin-Key': apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ reason: 'Admin deleted' }) + }); + + const data = await res.json(); + if (data.success) { + loadChat(); + } else { + alert('Failed to delete: ' + (data.message || 'Unknown error')); + } + } catch (e) { + console.error('Failed to delete message:', e); + alert('Error deleting message'); + } + } + + async function banUser(userId) { + const reason = prompt('Ban reason (optional):'); + if (reason === null) return; // Cancelled + + try { + const res = await fetch(`/api/admin/users/${userId}/chat-ban`, { + method: 'POST', + headers: { + 'X-Admin-Key': apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ banned: true, reason: reason || 'Admin banned' }) + }); + + const data = await res.json(); + if (data.success) { + loadChat(); + } else { + alert('Failed to ban: ' + (data.message || 'Unknown error')); + } + } catch (e) { + console.error('Failed to ban user:', e); + alert('Error banning user'); + } + } + + async function unbanUser(userId) { + if (!confirm('Unban this user from chat?')) return; + + try { + const res = await fetch(`/api/admin/users/${userId}/chat-ban`, { + method: 'POST', + headers: { + 'X-Admin-Key': apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ banned: false }) + }); + + const data = await res.json(); + if (data.success) { + loadChat(); + } else { + alert('Failed to unban: ' + (data.message || 'Unknown error')); + } + } catch (e) { + console.error('Failed to unban user:', e); + alert('Error unbanning user'); + } + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + function formatTimeAgo(date) { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 60) return 'just now'; + if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; + if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; + return Math.floor(seconds / 86400) + 'd ago'; + } + // Auto-refresh every 30 seconds setInterval(() => { if (apiKey && currentTab === 'overview') { diff --git a/html/index.html b/html/index.html index 4d7acc2..f888ecb 100644 --- a/html/index.html +++ b/html/index.html @@ -205,7 +205,8 @@
- + + 0/200
diff --git a/html/js/input/inputHandler.js b/html/js/input/inputHandler.js index 972d0e8..85281e5 100644 --- a/html/js/input/inputHandler.js +++ b/html/js/input/inputHandler.js @@ -21,22 +21,39 @@ const InputHandler = { }, setupChatForm: function() { + const MAX_CHAT_LENGTH = 200; + + // Character counter update + $('#chatInput').on('input', function() { + const len = $(this).val().length; + const counter = $('#charCounter'); + counter.text(len + '/' + MAX_CHAT_LENGTH); + // Change color when approaching limit + if (len >= MAX_CHAT_LENGTH) { + counter.css('color', '#f66'); + } else if (len >= MAX_CHAT_LENGTH * 0.8) { + counter.css('color', '#fa0'); + } else { + counter.css('color', '#666'); + } + }); + $('#chatForm').submit(function(e) { e.preventDefault(); var msg = $('#chatInput').val().trim(); - + // Don't send empty messages if (!msg) return false; - + // If the message is "enter", always send it to server // The server will handle queueing logic and timing if (msg.toLowerCase() === 'enter') { socket.emit('chat message', msg); - + // Don't automatically switch to waiting screen - let the server control screen state // The server will send appropriate events (payment_created, waiting_status, etc.) // to control what the user sees - + // Add visual feedback that request was sent $('#messages').append($('
  • ').text("🔑 Processing game entry request...")); UI.scrollChat(); @@ -44,8 +61,9 @@ const InputHandler = { // Send other chat messages normally socket.emit('chat message', msg); } - + $('#chatInput').val(''); + $('#charCounter').text('0/' + MAX_CHAT_LENGTH).css('color', '#666'); return false; }); diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index 0ca7463..92b39cb 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -120,7 +120,9 @@ const SocketHandlers = { socket.on('status_update', this.onStatusUpdate); socket.on('chat_broadcast', this.onChatBroadcast); socket.on('chat_history', this.onChatHistory); - + socket.on('chat_deleted', this.onChatDeleted); + socket.on('chat_error', this.onChatError); + // Game state handlers socket.on('waiting_status', this.onWaitingStatus); socket.on('queue_joined', this.onQueueJoined); @@ -298,27 +300,61 @@ const SocketHandlers = { onChatHistory: function(data) { if (!data || !data.messages || !Array.isArray(data.messages)) return; - + // Add a separator for history messages if (data.messages.length > 0) { $('#messages').append($('
  • ').text('--- Recent Chat History ---')); } - - // Add each historical message + + // Add each historical message with data-msg-id for deletion support data.messages.forEach(function(msg) { const msgElement = $('
  • '); - const username = msg.username || (msg.socketId ? msg.socketId.substring(0, 6) : 'anon'); - msgElement.html('' + username + ': ' + msg.message); + if (msg.id) { + msgElement.attr('data-msg-id', msg.id); + } + const username = msg.playerId || msg.username || (msg.socketId ? msg.socketId.substring(0, 6) : 'anon'); + const isSystem = msg.type === 'system' || msg.socketId === 'system'; + if (isSystem) { + msgElement.addClass('status'); + } + // Add timestamp if available + let timeStr = ''; + if (msg.timestamp) { + timeStr = '[' + SocketHandlers._formatTimeAgo(new Date(msg.timestamp)) + '] '; + } + msgElement.html(timeStr + '' + username + ': ' + msg.message); $('#messages').append(msgElement); }); - + if (data.messages.length > 0) { $('#messages').append($('
  • ').text(data.message)); + UI.scrollChat(); + }, + + _formatTimeAgo: function(date) { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 60) return 'just now'; + if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; + if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; + return Math.floor(seconds / 86400) + 'd ago'; + }, + onWaitingStatus: function(data) { if (data.status === 'waiting') { $('#messages').append($('
  • ').text(data.message)); diff --git a/src/index.js b/src/index.js index 16be69b..63ee8eb 100644 --- a/src/index.js +++ b/src/index.js @@ -996,6 +996,93 @@ app.post('/api/admin/queue/remove', adminAuth, asyncHandler(async (req, res) => }); })); +// ============================================================================= +// ADMIN CHAT MODERATION ENDPOINTS +// ============================================================================= + +/** + * GET /api/admin/chat + * List recent chat messages for moderation + * Query: ?limit=100&includeDeleted=false + */ +app.get('/api/admin/chat', adminAuth, asyncHandler(async (req, res) => { + const limit = Math.min(parseInt(req.query.limit) || 100, 500); + const includeDeleted = req.query.includeDeleted === 'true'; + + const messages = await socketHandlers.chatHandler.chatHistory.getMessagesForAdmin(limit, includeDeleted); + + res.json({ + messages, + count: messages.length + }); +})); + +/** + * DELETE /api/admin/chat/:id + * Soft delete a chat message + * Body: { reason?: string } + */ +app.delete('/api/admin/chat/:id', adminAuth, asyncHandler(async (req, res) => { + const messageId = parseInt(req.params.id); + const { reason } = req.body; + + if (!messageId || isNaN(messageId)) { + throw new ValidationError('Invalid message ID'); + } + + const deleted = await socketHandlers.chatHandler.chatHistory.deleteMessage( + messageId, + 'admin', + reason || 'Admin deleted' + ); + + if (deleted) { + // Broadcast deletion to all connected clients + io.emit('chat_deleted', { messageId }); + console.log(`[Admin] Chat message deleted: id=${messageId}, reason="${reason || 'No reason provided'}"`); + } + + res.json({ + success: deleted, + message: deleted ? 'Message deleted' : 'Message not found or already deleted' + }); +})); + +/** + * POST /api/admin/users/:id/chat-ban + * Ban or unban a user from chat + * Body: { banned: boolean, reason?: string } + */ +app.post('/api/admin/users/:id/chat-ban', adminAuth, asyncHandler(async (req, res) => { + const userId = parseInt(req.params.id); + const { banned, reason } = req.body; + + if (!userId || isNaN(userId)) { + throw new ValidationError('Invalid user ID'); + } + + if (typeof banned !== 'boolean') { + throw new ValidationError('banned must be a boolean'); + } + + await db.query(` + UPDATE users + SET chat_banned = $2, + chat_banned_at = CASE WHEN $2 THEN NOW() ELSE NULL END, + chat_banned_reason = $3 + WHERE id = $1 + `, [userId, banned, reason || null]); + + console.log(`[Admin] User ${userId} chat ${banned ? 'banned' : 'unbanned'}: reason="${reason || 'No reason provided'}"`); + + res.json({ + success: true, + userId, + banned, + reason: reason || null + }); +})); + /** * GET /api/admin/users/search * Search for users by socket ID prefix or payout address diff --git a/src/migrations/011_chat_moderation.sql b/src/migrations/011_chat_moderation.sql new file mode 100644 index 0000000..d9379cf --- /dev/null +++ b/src/migrations/011_chat_moderation.sql @@ -0,0 +1,44 @@ +-- Chat Moderation Migration +-- Adds moderation capabilities to chat system + +-- Add moderation fields to chat_messages +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id); +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS player_id VARCHAR(10); +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP; +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS deleted_by VARCHAR(255); +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS delete_reason VARCHAR(255); + +-- Index for non-deleted messages +CREATE INDEX IF NOT EXISTS idx_chat_messages_active +ON chat_messages(created_at DESC) +WHERE deleted_at IS NULL; + +-- Add chat ban fields to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS chat_banned BOOLEAN DEFAULT FALSE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS chat_banned_at TIMESTAMP; +ALTER TABLE users ADD COLUMN IF NOT EXISTS chat_banned_reason VARCHAR(255); + +-- Seed initial messages (only insert if table is empty or no system messages exist) +-- These give context and make the chat feel alive on fresh deploy +INSERT INTO chat_messages (socket_id, username, player_id, message, message_type, created_at) +SELECT 'system', 'SERVER', 'SERVER', msg, 'system', ts +FROM (VALUES + ('Welcome to Wownerogue! Use arrow keys or WASD to move.', NOW() - INTERVAL '1 hour'), + ('Escape the dungeon to win. Collect treasure for bonus payout!', NOW() - INTERVAL '59 minutes'), + ('Watch out for monsters - they''re fast!', NOW() - INTERVAL '58 minutes'), + ('Type /help for commands.', NOW() - INTERVAL '57 minutes'), + ('Good luck and have fun!', NOW() - INTERVAL '56 minutes') +) AS seed_data(msg, ts) +WHERE NOT EXISTS ( + SELECT 1 FROM chat_messages WHERE socket_id = 'system' LIMIT 1 +); + +-- Comment on new columns +COMMENT ON COLUMN chat_messages.user_id IS 'Reference to users table for registered users'; +COMMENT ON COLUMN chat_messages.player_id IS 'Display ID like PiMjKP shown in chat'; +COMMENT ON COLUMN chat_messages.deleted_at IS 'Soft delete timestamp for moderation'; +COMMENT ON COLUMN chat_messages.deleted_by IS 'Admin who deleted the message'; +COMMENT ON COLUMN chat_messages.delete_reason IS 'Reason for deletion'; +COMMENT ON COLUMN users.chat_banned IS 'Whether user is banned from chat'; +COMMENT ON COLUMN users.chat_banned_at IS 'When the chat ban was applied'; +COMMENT ON COLUMN users.chat_banned_reason IS 'Reason for chat ban'; diff --git a/src/network/chatHandler.js b/src/network/chatHandler.js index 48ad361..6d2c8d1 100644 --- a/src/network/chatHandler.js +++ b/src/network/chatHandler.js @@ -223,13 +223,42 @@ class ChatHandler { const trimmed = msg.trim(); if (!trimmed) return; + // Message length limit (reject, don't truncate) + const MAX_MESSAGE_LENGTH = 200; + if (trimmed.length > MAX_MESSAGE_LENGTH) { + this.broadcastManager.sendStatusUpdate(socket.id, 'warning', + `Message too long (${trimmed.length}/${MAX_MESSAGE_LENGTH} characters). Please shorten it.`); + return; + } + + // Get user info for ban check and user_id + let userId = null; + if (this.gameModeManager) { + try { + const userRow = await this.gameModeManager.getOrCreateUser(socket.id); + userId = userRow?.id || null; + + // Check if user is chat banned + if (userId && await this.chatHistory.isUserChatBanned(userId)) { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'You have been banned from chat.'); + return; + } + } catch (e) { + // Continue without user lookup - allow message + if (this.debugManager?.CONSOLE_LOGGING) { + console.warn('User lookup for chat failed:', e.message); + } + } + } + // Additional rate limit for broadcast messages (more restrictive) const now = Date.now(); const last = this._chatLastSent.get(socket.id) || 0; const BROADCAST_COOLDOWN = 2000; // 2 seconds between broadcasts - + if (now - last < BROADCAST_COOLDOWN) { - this.broadcastManager.sendStatusUpdate(socket.id, 'warning', + this.broadcastManager.sendStatusUpdate(socket.id, 'warning', `Please wait ${Math.ceil((BROADCAST_COOLDOWN - (now - last)) / 1000)} seconds before sending another message.`); return; } @@ -237,25 +266,27 @@ class ChatHandler { this._chatLastSent.set(socket.id, now); // Complete HTML entity escaping to prevent XSS + // Note: length already validated above, slice is just a safety net const safe = trimmed .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') - .slice(0, 300); + .slice(0, MAX_MESSAGE_LENGTH); const username = socket.id.substring(0, 6); - - // Save message to history (async, don't block broadcast) + + // Save message to history with user_id (async, don't block broadcast) this.chatHistory.saveMessage({ socketId: socket.id, username: username, message: safe, - type: 'chat' + type: 'chat', + userId: userId }).catch(err => { console.error('Failed to save chat message:', err.message); }); - + this.broadcastManager.broadcastChatMessage(username, safe, now, socket.id); } diff --git a/src/network/chatHistoryManager.js b/src/network/chatHistoryManager.js index c68bd7a..eb89b4b 100644 --- a/src/network/chatHistoryManager.js +++ b/src/network/chatHistoryManager.js @@ -20,7 +20,7 @@ class ChatHistoryManager { // Cleanup old messages periodically (once per day) this._cleanupInterval = null; - this._maxMessageAge = 30 * 24 * 60 * 60 * 1000; // 30 days in ms + this._maxMessageAge = 7 * 24 * 60 * 60 * 1000; // 7 days in ms } /** @@ -119,19 +119,23 @@ class ChatHistoryManager { * Save a chat message * @param {Object} messageData - Message data * @param {string} messageData.socketId - Socket ID of sender - * @param {string} messageData.username - Display username + * @param {string} messageData.username - Display username (playerId) * @param {string} messageData.message - Message content * @param {string} messageData.type - Message type (chat, system, event) + * @param {number} [messageData.userId] - Optional user ID from database * @returns {Object} Saved message with id and timestamp */ - async saveMessage({ socketId, username, message, type = 'chat' }) { + async saveMessage({ socketId, username, message, type = 'chat', userId = null }) { const timestamp = Date.now(); + const playerId = username; // username IS the playerId (6-char display ID) const messageObj = { socketId, username, + playerId, message, type, - timestamp + timestamp, + userId }; // Add to in-memory cache immediately @@ -139,7 +143,7 @@ class ChatHistoryManager { if (this._recentMessagesCache.length > this._cacheSize) { this._recentMessagesCache.shift(); } - + // Also add to fallback array this._inMemoryHistory.push(messageObj); if (this._inMemoryHistory.length > this._inMemoryFallbackSize) { @@ -150,11 +154,11 @@ class ChatHistoryManager { if (this.db) { try { const result = await this.db.query(` - INSERT INTO chat_messages (socket_id, username, message, message_type, created_at) - VALUES ($1, $2, $3, $4, NOW()) + INSERT INTO chat_messages (socket_id, username, player_id, user_id, message, message_type, created_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING id - `, [socketId, username, message, type]); - + `, [socketId, username, playerId, userId, message, type]); + messageObj.id = result.rows[0]?.id; } catch (error) { console.error('Failed to persist chat message:', error.message); @@ -167,36 +171,46 @@ class ChatHistoryManager { /** * Get recent messages for a new user - * @param {number} count - Number of messages to retrieve (default: 50) + * Uses hybrid strategy: last hour of messages, OR last 10 messages if < 10 in last hour + * @param {number} maxCount - Maximum number of messages to retrieve (default: 50) + * @param {number} minCount - Minimum messages to show if recent activity is low (default: 10) * @returns {Array} Array of recent messages */ - async getRecentMessages(count = 50) { - const limit = Math.min(count, this._cacheSize); - - // If cache is initialized, use it - if (this._cacheInitialized && this._recentMessagesCache.length > 0) { - return this._recentMessagesCache.slice(-limit); - } - - // If cache is empty, try in-memory fallback - if (this._inMemoryHistory.length > 0) { - return this._inMemoryHistory.slice(-limit); - } - - // If nothing in memory, try database + async getRecentMessages(maxCount = 50, minCount = 10) { + // Try database first for hybrid strategy if (this.db) { try { + // Hybrid query: Get messages from last hour, OR last N messages if fewer than minCount const result = await this.db.query(` - SELECT id, socket_id, username, message, message_type, created_at - FROM chat_messages - ORDER BY created_at DESC + WITH recent_hour AS ( + SELECT id, socket_id, username, player_id, message, message_type, created_at + FROM chat_messages + WHERE deleted_at IS NULL + AND created_at > NOW() - INTERVAL '1 hour' + ORDER BY created_at DESC + LIMIT $1 + ), + fallback AS ( + SELECT id, socket_id, username, player_id, message, message_type, created_at + FROM chat_messages + WHERE deleted_at IS NULL + ORDER BY created_at DESC + LIMIT $2 + ) + SELECT * FROM ( + SELECT * FROM recent_hour + UNION + SELECT * FROM fallback + ) combined + ORDER BY created_at ASC LIMIT $1 - `, [limit]); - - return result.rows.reverse().map(row => ({ + `, [maxCount, minCount]); + + return result.rows.map(row => ({ id: row.id, socketId: row.socket_id, username: row.username, + playerId: row.player_id || row.username, message: row.message, type: row.message_type, timestamp: new Date(row.created_at).getTime() @@ -205,7 +219,21 @@ class ChatHistoryManager { console.error('Failed to get recent messages from DB:', error.message); } } - + + // Fallback to cache (filter out deleted messages) + const limit = Math.min(maxCount, this._cacheSize); + + if (this._cacheInitialized && this._recentMessagesCache.length > 0) { + const activeMessages = this._recentMessagesCache.filter(m => !m.deletedAt); + return activeMessages.slice(-limit); + } + + // Final fallback to in-memory + if (this._inMemoryHistory.length > 0) { + const activeMessages = this._inMemoryHistory.filter(m => !m.deletedAt); + return activeMessages.slice(-limit); + } + return []; } @@ -237,6 +265,102 @@ class ChatHistoryManager { } } + /** + * Check if a user is banned from chat + * @param {number} userId - User ID to check + * @returns {boolean} True if user is banned + */ + async isUserChatBanned(userId) { + if (!userId || !this.db) return false; + + try { + const result = await this.db.query( + 'SELECT chat_banned FROM users WHERE id = $1', + [userId] + ); + return result.rows[0]?.chat_banned === true; + } catch (error) { + console.error('Failed to check chat ban status:', error.message); + return false; + } + } + + /** + * Soft delete a message (admin moderation) + * @param {number} messageId - Message ID to delete + * @param {string} deletedBy - Admin identifier + * @param {string} reason - Reason for deletion + * @returns {boolean} True if deleted successfully + */ + async deleteMessage(messageId, deletedBy = 'admin', reason = null) { + if (!this.db) return false; + + try { + const result = await this.db.query(` + UPDATE chat_messages + SET deleted_at = NOW(), deleted_by = $2, delete_reason = $3 + WHERE id = $1 AND deleted_at IS NULL + RETURNING id + `, [messageId, deletedBy, reason]); + + if (result.rowCount > 0) { + // Also remove from in-memory caches + this._recentMessagesCache = this._recentMessagesCache.filter(m => m.id !== messageId); + this._inMemoryHistory = this._inMemoryHistory.filter(m => m.id !== messageId); + return true; + } + return false; + } catch (error) { + console.error('Failed to delete chat message:', error.message); + return false; + } + } + + /** + * Get messages for admin view (includes deleted) + * @param {number} limit - Number of messages + * @param {boolean} includeDeleted - Whether to include deleted messages + * @returns {Array} Messages for admin + */ + async getMessagesForAdmin(limit = 100, includeDeleted = false) { + if (!this.db) return []; + + try { + const result = await this.db.query(` + SELECT + c.id, c.socket_id, c.username, c.player_id, c.user_id, + c.message, c.message_type, c.created_at, + c.deleted_at, c.deleted_by, c.delete_reason, + u.payout_address, u.chat_banned + FROM chat_messages c + LEFT JOIN users u ON c.user_id = u.id + ${includeDeleted ? '' : 'WHERE c.deleted_at IS NULL'} + ORDER BY c.created_at DESC + LIMIT $1 + `, [limit]); + + return result.rows.map(row => ({ + id: row.id, + socketId: row.socket_id, + username: row.username, + playerId: row.player_id, + userId: row.user_id, + message: row.message, + type: row.message_type, + timestamp: new Date(row.created_at).getTime(), + createdAt: row.created_at, + deletedAt: row.deleted_at, + deletedBy: row.deleted_by, + deleteReason: row.delete_reason, + payoutAddress: row.payout_address, + chatBanned: row.chat_banned + })); + } catch (error) { + console.error('Failed to get admin messages:', error.message); + return []; + } + } + /** * Get message count for statistics * @returns {Object} Statistics about chat messages