From bc8a04825bb59954b3b14d606610067186f291ca Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Mon, 8 Dec 2025 06:35:49 +0000 Subject: [PATCH] Add command logging and real-time dashboard updates Integrates PostgreSQL for command logging and analytics, adds WebSocket support for real-time dashboard updates, and includes `pg` and `ws` dependencies. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 2d4b80d0-ca85-407f-99c3-f3596476f525 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/4ZEVdt6 Replit-Helium-Checkpoint-Created: true --- .replit | 4 - aethex-bot/bot.js | 325 ++++++++++++++++++++++++++++++++++++++++++++-- package-lock.json | 184 ++++++++++++++++++++++++++ package.json | 16 +++ 4 files changed, 517 insertions(+), 12 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.replit b/.replit index 9ca92fe..5b6f647 100644 --- a/.replit +++ b/.replit @@ -22,10 +22,6 @@ externalPort = 80 localPort = 8080 externalPort = 8080 -[[ports]] -localPort = 38859 -externalPort = 3000 - [workflows] runButton = "Project" diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index 82c772b..432d059 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -12,6 +12,8 @@ const { createClient } = require("@supabase/supabase-js"); const http = require("http"); const fs = require("fs"); const path = require("path"); +const { Pool } = require("pg"); +const WebSocket = require("ws"); // Dashboard HTML path const dashboardPath = path.join(__dirname, "public", "dashboard.html"); @@ -66,6 +68,181 @@ if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) { console.log("Supabase not configured - community features will be limited"); } +// ============================================================================= +// POSTGRESQL DATABASE SETUP (Local database for analytics & config) +// ============================================================================= + +let pgPool = null; +if (process.env.DATABASE_URL) { + pgPool = new Pool({ + connectionString: process.env.DATABASE_URL, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + }); + pgPool.on('error', (err) => { + console.error('PostgreSQL pool error:', err.message); + }); + console.log("PostgreSQL connected"); +} else { + console.log("PostgreSQL not configured - using in-memory storage"); +} + +// ============================================================================= +// COMMAND LOGGING SYSTEM +// ============================================================================= + +async function logCommand(data) { + if (!pgPool) return; + try { + await pgPool.query( + `INSERT INTO command_logs (command_name, user_id, user_tag, guild_id, guild_name, channel_id, success, error_message, execution_time_ms) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + [ + data.commandName, + data.userId, + data.userTag, + data.guildId, + data.guildName, + data.channelId, + data.success, + data.errorMessage || null, + data.executionTime || null + ] + ); + } catch (err) { + console.error('Failed to log command:', err.message); + } +} + +async function getCommandAnalytics(days = 7) { + if (!pgPool) return { commands: [], hourly: [], daily: [], topUsers: [] }; + try { + const commandsResult = await pgPool.query( + `SELECT command_name, COUNT(*) as count, + SUM(CASE WHEN success THEN 1 ELSE 0 END) as success_count, + AVG(execution_time_ms) as avg_time + FROM command_logs + WHERE created_at > NOW() - INTERVAL '${days} days' + GROUP BY command_name + ORDER BY count DESC + LIMIT 20` + ); + + const hourlyResult = await pgPool.query( + `SELECT EXTRACT(HOUR FROM created_at) as hour, COUNT(*) as count + FROM command_logs + WHERE created_at > NOW() - INTERVAL '24 hours' + GROUP BY hour + ORDER BY hour` + ); + + const dailyResult = await pgPool.query( + `SELECT DATE(created_at) as date, COUNT(*) as count + FROM command_logs + WHERE created_at > NOW() - INTERVAL '${days} days' + GROUP BY date + ORDER BY date` + ); + + const topUsersResult = await pgPool.query( + `SELECT user_id, user_tag, COUNT(*) as command_count + FROM command_logs + WHERE created_at > NOW() - INTERVAL '${days} days' + GROUP BY user_id, user_tag + ORDER BY command_count DESC + LIMIT 10` + ); + + return { + commands: commandsResult.rows, + hourly: hourlyResult.rows, + daily: dailyResult.rows, + topUsers: topUsersResult.rows + }; + } catch (err) { + console.error('Failed to get command analytics:', err.message); + return { commands: [], hourly: [], daily: [], topUsers: [] }; + } +} + +async function getTotalCommandCount() { + if (!pgPool) return 0; + try { + const result = await pgPool.query('SELECT COUNT(*) as count FROM command_logs'); + return parseInt(result.rows[0].count) || 0; + } catch (err) { + return 0; + } +} + +// PostgreSQL-based server config functions +async function saveServerConfigToDB(guildId, config) { + if (!pgPool) return false; + try { + await pgPool.query( + `INSERT INTO server_config (guild_id, welcome_channel, goodbye_channel, modlog_channel, level_up_channel, auto_role, verified_role, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) + ON CONFLICT (guild_id) DO UPDATE SET + welcome_channel = EXCLUDED.welcome_channel, + goodbye_channel = EXCLUDED.goodbye_channel, + modlog_channel = EXCLUDED.modlog_channel, + level_up_channel = EXCLUDED.level_up_channel, + auto_role = EXCLUDED.auto_role, + verified_role = EXCLUDED.verified_role, + updated_at = NOW()`, + [guildId, config.welcome_channel, config.goodbye_channel, config.modlog_channel, + config.level_up_channel, config.auto_role, config.verified_role] + ); + return true; + } catch (err) { + console.error('Failed to save server config:', err.message); + return false; + } +} + +async function getServerConfigFromDB(guildId) { + if (!pgPool) return null; + try { + const result = await pgPool.query( + 'SELECT * FROM server_config WHERE guild_id = $1', + [guildId] + ); + return result.rows[0] || null; + } catch (err) { + console.error('Failed to get server config:', err.message); + return null; + } +} + +// Federation mappings with PostgreSQL +async function saveFederationMappingToDB(guildId, roleId, roleName) { + if (!pgPool) return false; + try { + await pgPool.query( + `INSERT INTO federation_mappings (guild_id, role_id, role_name, linked_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (guild_id, role_id) DO UPDATE SET role_name = EXCLUDED.role_name`, + [guildId, roleId, roleName] + ); + return true; + } catch (err) { + console.error('Failed to save federation mapping:', err.message); + return false; + } +} + +async function getFederationMappingsFromDB() { + if (!pgPool) return []; + try { + const result = await pgPool.query('SELECT * FROM federation_mappings ORDER BY linked_at DESC'); + return result.rows; + } catch (err) { + console.error('Failed to get federation mappings:', err.message); + return []; + } +} + // ============================================================================= // SENTINEL: HEAT TRACKING SYSTEM (New) // ============================================================================= @@ -588,24 +765,47 @@ client.on("interactionCreate", async (interaction) => { } const queueEntry = addToCommandQueue(`/${interaction.commandName} by ${interaction.user.tag}`, 'pending'); + const startTime = Date.now(); try { console.log(`[Command] Executing: ${interaction.commandName}`); await command.execute(interaction, supabase, client); - console.log(`[Command] Completed: ${interaction.commandName}`); + const executionTime = Date.now() - startTime; + console.log(`[Command] Completed: ${interaction.commandName} (${executionTime}ms)`); updateCommandQueue(queueEntry.id, 'completed'); trackCommand(interaction.commandName); resetDailyAnalytics(); - addActivity('command', { + const activityData = { command: interaction.commandName, user: interaction.user.tag, userId: interaction.user.id, guild: interaction.guild?.name || 'DM', guildId: interaction.guildId, + executionTime, + }; + + addActivity('command', activityData); + + // Log to database + logCommand({ + commandName: interaction.commandName, + userId: interaction.user.id, + userTag: interaction.user.tag, + guildId: interaction.guildId, + guildName: interaction.guild?.name || 'DM', + channelId: interaction.channelId, + success: true, + executionTime, }); + + // Broadcast via WebSocket + if (typeof wsBroadcast === 'function') { + wsBroadcast('command', activityData); + } } catch (error) { + const executionTime = Date.now() - startTime; console.error(`Error executing ${interaction.commandName}:`, error); updateCommandQueue(queueEntry.id, 'failed'); @@ -617,6 +817,19 @@ client.on("interactionCreate", async (interaction) => { error: error.message, }); + // Log failed command to database + logCommand({ + commandName: interaction.commandName, + userId: interaction.user.id, + userTag: interaction.user.tag, + guildId: interaction.guildId, + guildName: interaction.guild?.name || 'DM', + channelId: interaction.channelId, + success: false, + errorMessage: error.message, + executionTime, + }); + try { const errorEmbed = new EmbedBuilder() .setColor(0xff0000) @@ -758,8 +971,7 @@ const checkAdminAuth = (req) => { return authHeader === `Bearer ${ADMIN_TOKEN}`; }; -http - .createServer((req, res) => { +const httpServer = http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); @@ -1988,14 +2200,111 @@ http return; } + // Analytics endpoint with detailed command data + if (req.url === "/command-analytics" || req.url.startsWith("/command-analytics?")) { + (async () => { + try { + const url = new URL(req.url, `http://localhost:${healthPort}`); + const days = parseInt(url.searchParams.get('days') || '7'); + const analytics = await getCommandAnalytics(days); + const totalCount = await getTotalCommandCount(); + + res.writeHead(200); + res.end(JSON.stringify({ + success: true, + totalCommands: totalCount, + analytics, + timestamp: new Date().toISOString(), + })); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ success: false, error: error.message })); + } + })(); + return; + } + res.writeHead(404); res.end(JSON.stringify({ error: "Not found" })); - }) - .listen(healthPort, () => { - console.log(`Health check server running on port ${healthPort}`); - console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`); }); +// ============================================================================= +// WEBSOCKET SERVER FOR REAL-TIME UPDATES +// ============================================================================= + +const wsClients = new Set(); +const wss = new WebSocket.Server({ noServer: true }); + +wss.on('connection', (ws) => { + wsClients.add(ws); + console.log(`[WebSocket] Client connected. Total: ${wsClients.size}`); + + ws.send(JSON.stringify({ + type: 'init', + data: { + status: 'online', + guilds: client.guilds.cache.size, + commands: client.commands?.size || 0, + uptime: Math.floor(process.uptime()), + heatMapSize: heatMap.size, + activeTickets: activeTickets.size, + federationLinks: federationMappings.size, + } + })); + + ws.on('close', () => { + wsClients.delete(ws); + console.log(`[WebSocket] Client disconnected. Total: ${wsClients.size}`); + }); + + ws.on('error', (err) => { + console.error('[WebSocket] Error:', err.message); + wsClients.delete(ws); + }); +}); + +function wsBroadcast(type, data) { + const message = JSON.stringify({ type, data, timestamp: new Date().toISOString() }); + for (const wsClient of wsClients) { + if (wsClient.readyState === WebSocket.OPEN) { + try { + wsClient.send(message); + } catch (err) { + console.error('[WebSocket] Broadcast error:', err.message); + } + } + } +} + +setInterval(() => { + if (wsClients.size > 0) { + wsBroadcast('stats', { + guilds: client.guilds.cache.size, + commands: client.commands?.size || 0, + uptime: Math.floor(process.uptime()), + heatMapSize: heatMap.size, + activeTickets: activeTickets.size, + federationLinks: federationMappings.size, + memory: process.memoryUsage(), + cpu: getCpuUsage(), + }); + } +}, 5000); + +// Add WebSocket upgrade handling +httpServer.on('upgrade', (request, socket, head) => { + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); +}); + +httpServer.listen(healthPort, () => { + console.log(`Health check server running on port ${healthPort}`); + console.log(`WebSocket server available at ws://localhost:${healthPort}`); + console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`); +}); + + // ============================================================================= // BOT LOGIN AND READY // ============================================================================= diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..471ebd4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,184 @@ +{ + "name": "workspace", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "workspace", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "pg": "^8.16.3", + "ws": "^8.18.3" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5520c6b --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "workspace", + "version": "1.0.0", + "description": "This is a starting point for making your own Discord bot using Python and the [discordpy](https://discordpy.readthedocs.io/) library. Read [their getting-started guides](https://discordpy.readthedocs.io/en/stable/#getting-started) to get the most out of this template.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "pg": "^8.16.3", + "ws": "^8.18.3" + } +}