const Discord = require('discord.js'); const Docker = require('dockerode'); const { CronJob } = require('cron'); const { execSync } = require('child_process'); const fs = require('fs').promises; // Added for file operations require('dotenv').config(); const client = new Discord.Client({ intents: [ Discord.GatewayIntentBits.Guilds, Discord.GatewayIntentBits.GuildMembers ] }); const docker = new Docker(); // Assumes local Docker socket; adjust if remote const DISCORD_TOKEN = process.env.DISCORD_TOKEN; const GUILD_ID = process.env.GUILD_ID; const ROLE_IDS = { standard: process.env.ROLE_ID_STANDARD, manualUpgrade: process.env.ROLE_ID_MANUAL_UPGRADE, superUpgrade: process.env.ROLE_ID_SUPER_UPGRADE }; const DEFAULT_CPUS = parseInt(process.env.DEFAULT_CPUS); const DEFAULT_MEMORY = parseInt(process.env.DEFAULT_MEMORY) * 1024 * 1024; // Convert MiB to bytes const DEFAULT_SWAP = parseInt(process.env.DEFAULT_SWAP) * 1024 * 1024; // Convert MiB to bytes const UPGRADED_CPUS = parseInt(process.env.UPGRADED_CPUS); const UPGRADED_MEMORY = parseInt(process.env.UPGRADED_MEMORY) * 1024 * 1024; // Convert MiB to bytes const UPGRADED_SWAP = parseInt(process.env.UPGRADED_SWAP) * 1024 * 1024; // Convert MiB to bytes const SUPER_UPGRADED_CPUS = parseInt(process.env.SUPER_UPGRADED_CPUS); const SUPER_UPGRADED_MEMORY = parseInt(process.env.SUPER_UPGRADED_MEMORY) * 1024 * 1024; // Convert MiB to bytes const SUPER_UPGRADED_SWAP = parseInt(process.env.SUPER_UPGRADED_SWAP) * 1024 * 1024; // Convert MiB to bytes const RESET_UNKNOWN_TO_DEFAULT = process.env.RESET_UNKNOWN_TO_DEFAULT === 'true'; const EXEC_TIMEOUT = parseInt(process.env.EXEC_TIMEOUT); const CACHE_FILE = '/var/www/html/current_upgraded.json'; // Cache file path async function execWithTimeout(container, cmd, timeout = EXEC_TIMEOUT) { try { const exec = await container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }); const stream = await exec.start(); let output = ''; const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`Command ${cmd.join(' ')} timed out after ${timeout}ms`)), timeout); }); await Promise.race([ new Promise((resolve, reject) => { stream.on('data', (chunk) => output += chunk.toString()); stream.on('end', () => resolve(output)); stream.on('error', reject); }), timeoutPromise ]); return output; } catch (err) { throw err; } } async function updateContainerConfig(container, name, userId, memoryLimit) { try { let sourceFile; if (memoryLimit === SUPER_UPGRADED_MEMORY) { sourceFile = 'startServer_superUpgrade.json'; } else if (memoryLimit === UPGRADED_MEMORY) { sourceFile = 'startServer_upgrade.json'; } else { sourceFile = 'startServer_downgrade.json'; } console.log(` šŸ”„ Updating container ${name} with ${sourceFile}...`); // Check if startServer.json exists console.log(` šŸ” Checking if startServer.json exists in container ${name}...`); try { await execWithTimeout(container, ['test', '-f', '/var/tools/pm2/startServer.json']); console.log(` āœ… startServer.json exists`); // Remove existing startServer.json console.log(` šŸ—‘ļø Removing existing startServer.json in container ${name}...`); await execWithTimeout(container, ['rm', '-f', '/var/tools/pm2/startServer.json']); console.log(` āœ… Removed startServer.json`); } catch (err) { console.log(` āš ļø startServer.json does not exist or cannot be removed: ${err.message}`); } // Copy new startServer.json file console.log(` šŸ“¤ Copying ${sourceFile} to container ${name}...`); try { execSync(`docker cp ${sourceFile} ${name}:/var/tools/pm2/startServer.json`, { stdio: 'inherit', timeout: EXEC_TIMEOUT }); console.log(` āœ… Copied ${sourceFile} to /var/tools/pm2/startServer.json`); } catch (cpErr) { console.error(` āŒ Error copying ${sourceFile} to container ${name}: ${cpErr.message}`); return; } // Delete existing PM2 process console.log(` šŸ—‘ļø Deleting PM2 process in container ${name}...`); try { await execWithTimeout(container, ['su', '-', 'mc', '-c', 'cd /var/tools/pm2 && pm2 delete 0']); console.log(` āœ… PM2 process deleted`); } catch (err) { console.error(` āš ļø Error deleting PM2 process in container ${name}: ${err.message}`); // Continue to attempt starting the process } // Start new PM2 process console.log(` ā–¶ļø Starting PM2 process in container ${name}...`); try { await execWithTimeout(container, ['su', '-', 'mc', '-c', 'cd /var/tools/pm2 && pm2 start startServer.json']); console.log(` āœ… PM2 process started with startServer.json`); } catch (err) { console.error(` āŒ Error starting PM2 process in container ${name}: ${err.message}`); return; } } catch (err) { console.error(` āŒ Error updating container ${name}: ${err.message}`); } } async function updateCache(upgradedContainers) { try { const data = JSON.stringify(upgradedContainers, null, 2); await fs.writeFile(CACHE_FILE, data); console.log(` āœ… Cache updated at ${CACHE_FILE} with ${upgradedContainers.length} upgraded containers`); } catch (err) { console.error(` āŒ Error writing to cache file ${CACHE_FILE}: ${err.message}`); } } async function checkContainers() { console.log('\n=== Starting Container Check ===\n'); const upgradedContainers = []; // Track upgraded containers for cache try { // List running containers const containers = await docker.listContainers({ all: false }); for (const contInfo of containers) { // Container names start with '/', e.g., '/mc_1234567890' const name = contInfo.Names[0].slice(1); if (name.startsWith('mc_')) { const userId = name.slice(3); // Extract Discord ID const container = docker.getContainer(contInfo.Id); const inspect = await container.inspect(); const currentCpus = inspect.HostConfig.NanoCpus / 1e9; const currentMem = inspect.HostConfig.Memory; const currentSwap = inspect.HostConfig.MemorySwap; // Log container details in a structured format console.log(`šŸ“¦ Container: ${name}`); console.log(` User ID: ${userId}`); console.log(` Current Settings:`); console.log(` CPUs: ${currentCpus}`); console.log(` Memory: ${currentMem / 1024 / 1024} MiB`); console.log(` Swap: ${currentSwap / 1024 / 1024} MiB`); const isDefault = currentCpus === DEFAULT_CPUS && currentMem === DEFAULT_MEMORY && currentSwap === DEFAULT_SWAP; const isUpgraded = currentCpus === UPGRADED_CPUS && currentMem === UPGRADED_MEMORY && currentSwap === UPGRADED_SWAP; const isSuperUpgraded = currentCpus === SUPER_UPGRADED_CPUS && currentMem === SUPER_UPGRADED_MEMORY && currentSwap === SUPER_UPGRADED_SWAP; // Track upgraded containers if (isUpgraded || isSuperUpgraded) { upgradedContainers.push({ containerName: name, userId: userId, upgradeType: isSuperUpgraded ? 'super' : 'standard' }); } // Handle unknown limits if (!isDefault && !isUpgraded && !isSuperUpgraded) { console.log(` āš ļø Warning: Unknown limits detected!`); console.log(` Expected Default: CPUs=${DEFAULT_CPUS}, Memory=${DEFAULT_MEMORY / 1024 / 1024} MiB, Swap=${DEFAULT_SWAP / 1024 / 1024} MiB`); console.log(` Expected Upgraded: CPUs=${UPGRADED_CPUS}, Memory=${UPGRADED_MEMORY / 1024 / 1024} MiB, Swap=${UPGRADED_SWAP / 1024 / 1024} MiB`); console.log(` Expected Super Upgraded: CPUs=${SUPER_UPGRADED_CPUS}, Memory=${SUPER_UPGRADED_MEMORY / 1024 / 1024} MiB, Swap=${SUPER_UPGRADED_SWAP / 1024 / 1024} MiB`); if (RESET_UNKNOWN_TO_DEFAULT) { console.log(` šŸ”„ Resetting to default settings...`); await container.update({ NanoCpus: DEFAULT_CPUS * 1e9, Memory: DEFAULT_MEMORY, MemorySwap: DEFAULT_SWAP }); await updateContainerConfig(container, name, userId, DEFAULT_MEMORY); console.log(` āœ… Container reset to default settings.`); } else { console.log(` ā­ļø Skipping due to unknown limits.`); continue; } } // Fetch guild and check roles const guild = client.guilds.cache.get(GUILD_ID); if (!guild) { console.log(` āŒ Guild ${GUILD_ID} not found.`); continue; } let hasSuperUpgradeRole = false; let hasStandardOrManualRole = false; try { const member = await guild.members.fetch(userId); hasSuperUpgradeRole = member.roles.cache.has(ROLE_IDS.superUpgrade); hasStandardOrManualRole = [ROLE_IDS.standard, ROLE_IDS.manualUpgrade].some(roleId => member.roles.cache.has(roleId)); console.log(` Role Check: User ${hasSuperUpgradeRole ? 'has superUpgrade role' : hasStandardOrManualRole ? 'has standard or manual upgrade role' : 'has no relevant roles'} (${Object.values(ROLE_IDS).join(' or ')})`); } catch (err) { console.log(` āŒ Error fetching member ${userId}: ${err.message}`); continue; } if (hasSuperUpgradeRole && !isSuperUpgraded) { // Apply super upgrade console.log(` šŸ”¼ Applying super upgrade to container...`); await container.update({ NanoCpus: SUPER_UPGRADED_CPUS * 1e9, Memory: SUPER_UPGRADED_MEMORY, MemorySwap: SUPER_UPGRADED_SWAP }); await updateContainerConfig(container, name, userId, SUPER_UPGRADED_MEMORY); console.log(` āœ… Super Upgraded to: CPUs=${SUPER_UPGRADED_CPUS}, Memory=${SUPER_UPGRADED_MEMORY / 1024 / 1024} MiB, Swap=${SUPER_UPGRADED_SWAP / 1024 / 1024} MiB`); upgradedContainers.push({ containerName: name, userId: userId, upgradeType: 'super' }); } else if (!hasSuperUpgradeRole && hasStandardOrManualRole && !isUpgraded) { // Apply standard upgrade console.log(` šŸ”¼ Upgrading container...`); await container.update({ NanoCpus: UPGRADED_CPUS * 1e9, Memory: UPGRADED_MEMORY, MemorySwap: UPGRADED_SWAP }); await updateContainerConfig(container, name, userId, UPGRADED_MEMORY); console.log(` āœ… Upgraded to: CPUs=${UPGRADED_CPUS}, Memory=${UPGRADED_MEMORY / 1024 / 1024} MiB, Swap=${UPGRADED_SWAP / 1024 / 1024} MiB`); upgradedContainers.push({ containerName: name, userId: userId, upgradeType: 'standard' }); } else if (!hasSuperUpgradeRole && !hasStandardOrManualRole && (isUpgraded || isSuperUpgraded)) { // Downgrade console.log(` šŸ”½ Downgrading container...`); await container.update({ NanoCpus: DEFAULT_CPUS * 1e9, Memory: DEFAULT_MEMORY, MemorySwap: DEFAULT_SWAP }); await updateContainerConfig(container, name, userId, DEFAULT_MEMORY); console.log(` āœ… Downgraded to: CPUs=${DEFAULT_CPUS}, Memory=${DEFAULT_MEMORY / 1024 / 1024} MiB, Swap=${DEFAULT_SWAP / 1024 / 1024} MiB`); // Remove from upgradedContainers if present const index = upgradedContainers.findIndex(c => c.containerName === name); if (index !== -1) upgradedContainers.splice(index, 1); } else { console.log(` āœ… No action needed. Container settings match role status.`); } console.log('----------------------------------------'); } } // Update cache file with upgraded containers await updateCache(upgradedContainers); console.log('\n=== Container Check Completed ===\n'); } catch (err) { console.error(`\nāŒ Error in container check: ${err.message}\n`); } } client.once('ready', () => { console.log(`āœ… Logged in as ${client.user.tag}. Bot is ready.`); // Run initial check on startup checkContainers(); // Cron job every 5 minutes const job = new CronJob('*/30 * * * *', checkContainers, null, true, 'UTC'); job.start(); }); client.login(DISCORD_TOKEN);