const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const mineflayer = require('mineflayer'); const fetch = require('node-fetch'); const { parse } = require('csv-parse/sync'); const path = require('path'); const dns = require('dns').promises; const app = express(); const server = http.createServer(app); const io = socketIo(server); // Google Sheets configuration const SHEET_ID = '109roJQr-Y4YCLTkCqaK6iwShC-Dr2Jb-hB0qE2phNqQ'; const SHEET_URL = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/export?format=csv`; // Bot management const bots = new Map(); const serverBotMap = new Map(); // Track one bot per server // Function to resolve domain to IP async function resolveToIP(hostname) { try { // Check if it's already an IP address if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) { return hostname; } // Resolve domain to IP const addresses = await dns.resolve4(hostname); return addresses[0]; // Return first IP } catch (error) { console.error(`Failed to resolve ${hostname}:`, error.message); return hostname; // Return original if resolution fails } } class BotManager { constructor(botName, ip, port, version) { this.botName = botName; this.ip = ip; this.port = port; this.version = version || '1.20.1'; this.bot = null; this.status = 'Disconnected'; this.deathCount = 0; this.disconnectTime = null; this.lastReconnectTime = null; this.isManualDisconnect = false; this.startTime = Date.now(); this.connectedTime = null; this.inSheet = true; this.resolvedIP = null; } async connect() { try { // Resolve domain to IP for comparison this.resolvedIP = await resolveToIP(this.ip); const serverKey = `${this.resolvedIP}:${this.port}`; // Check if server already has a bot const existingBot = serverBotMap.get(serverKey); if (existingBot && existingBot !== this.botName) { this.status = 'Server already has a bot'; console.log(`Bot ${this.botName} blocked: Server ${serverKey} already has bot ${existingBot}`); return false; } this.status = 'Connecting...'; this.bot = mineflayer.createBot({ host: this.ip, // Use original hostname/IP for connection port: parseInt(this.port), username: this.botName, auth: 'offline', version: this.version, hideErrors: true, checkTimeoutInterval: 30000 }); // Register this bot for the server using resolved IP serverBotMap.set(serverKey, this.botName); console.log(`Registering bot ${this.botName} for server ${serverKey}`); this.bot.once('spawn', () => { this.status = 'Connected'; this.connectedTime = Date.now(); this.disconnectTime = null; console.log(`Bot ${this.botName} spawned on ${this.ip}:${this.port} (resolved: ${this.resolvedIP})`); // Start AFK behavior this.startAFK(); }); this.bot.on('death', () => { this.deathCount++; this.status = 'Dead'; console.log(`Bot ${this.botName} died. Total deaths: ${this.deathCount}`); this.handleDisconnect(); }); this.bot.on('kicked', (reason) => { console.log(`Bot ${this.botName} was kicked: ${reason}`); this.handleDisconnect(); }); this.bot.on('error', (err) => { console.error(`Bot ${this.botName} error:`, err.message); }); this.bot.on('end', () => { this.handleDisconnect(); }); return true; } catch (error) { console.error(`Failed to connect bot ${this.botName}:`, error); this.status = 'Connection Failed'; // Clean up server registration if connection failed if (this.resolvedIP) { const serverKey = `${this.resolvedIP}:${this.port}`; if (serverBotMap.get(serverKey) === this.botName) { serverBotMap.delete(serverKey); } } return false; } } handleDisconnect() { if (this.resolvedIP) { const serverKey = `${this.resolvedIP}:${this.port}`; if (serverBotMap.get(serverKey) === this.botName) { serverBotMap.delete(serverKey); console.log(`Unregistering bot ${this.botName} from server ${serverKey}`); } } if (this.status !== 'Dead') { this.status = 'Disconnected'; } this.disconnectTime = Date.now(); this.connectedTime = null; this.bot = null; console.log(`Bot ${this.botName} disconnected from ${this.ip}:${this.port}`); } disconnect() { this.isManualDisconnect = true; if (this.bot) { this.bot.quit(); } this.handleDisconnect(); } startAFK() { if (!this.bot) return; // Simple AFK movement let direction = 1; const afkInterval = setInterval(() => { if (!this.bot || this.status !== 'Connected') { clearInterval(afkInterval); return; } // Walk forward and backward this.bot.setControlState('forward', direction > 0); this.bot.setControlState('back', direction < 0); setTimeout(() => { if (this.bot) { this.bot.clearControlStates(); } }, 1000); direction *= -1; }, 5000); } canReconnect() { if (this.status === 'Connected' || this.status === 'Connecting...') return false; if (!this.inSheet) return false; if (!this.lastReconnectTime) return true; const hourAgo = Date.now() - (60 * 60 * 1000); return this.lastReconnectTime < hourAgo; } async reconnect() { if (!this.canReconnect()) { return false; } this.lastReconnectTime = Date.now(); this.isManualDisconnect = false; return await this.connect(); } getTimeUntilReconnect() { if (!this.lastReconnectTime) return 0; const timeElapsed = Date.now() - this.lastReconnectTime; const hourInMs = 60 * 60 * 1000; const timeRemaining = Math.max(0, hourInMs - timeElapsed); return Math.ceil(timeRemaining / 1000); } getConnectedDuration() { if (!this.connectedTime || this.status !== 'Connected') return 0; return Math.floor((Date.now() - this.connectedTime) / 1000); } getInfo() { return { botName: this.botName, status: this.status, deathCount: this.deathCount, connectedDuration: this.getConnectedDuration(), canReconnect: this.canReconnect(), disconnectTime: this.disconnectTime, timeUntilReconnect: this.getTimeUntilReconnect(), inSheet: this.inSheet }; } } // Fetch and parse Google Sheets data async function fetchSheetData() { try { const response = await fetch(SHEET_URL); const csvText = await response.text(); const records = parse(csvText, { columns: true, skip_empty_lines: true }); return records; } catch (error) { console.error('Error fetching sheet data:', error); return []; } } // Update bots based on sheet data async function updateBots() { const sheetData = await fetchSheetData(); const activeBots = new Set(); // Mark all existing bots as not in sheet initially for (const [botName, botManager] of bots.entries()) { botManager.inSheet = false; } for (const row of sheetData) { const botName = row['BOT NAME']?.trim(); const ip = row['IP']?.trim(); const port = row['PORT']?.trim(); const version = row['Version']?.trim() || '1.20.1'; if (!botName || !ip || !port) continue; activeBots.add(botName); // Add new bot if it doesn't exist if (!bots.has(botName)) { const botManager = new BotManager(botName, ip, port, version); bots.set(botName, botManager); console.log(`Added new bot from sheet: ${botName} for ${ip}:${port}`); } else { // Mark existing bot as still in sheet bots.get(botName).inSheet = true; } } // Remove bots that are no longer in the sheet for (const [botName, botManager] of bots.entries()) { if (!botManager.inSheet) { console.log(`Removing bot ${botName} - no longer in sheet`); botManager.disconnect(); bots.delete(botName); } } console.log(`Total bots: ${bots.size}, Active servers: ${serverBotMap.size}`); } // Socket.IO events io.on('connection', (socket) => { console.log('Client connected'); // Send initial bot data const sendBotData = () => { const botData = Array.from(bots.values()).map(bot => bot.getInfo()); socket.emit('botUpdate', botData); }; sendBotData(); const updateInterval = setInterval(sendBotData, 2000); socket.on('reconnectBot', async (botName) => { const botManager = bots.get(botName); if (botManager) { const success = await botManager.reconnect(); socket.emit('reconnectResult', { botName, success }); } }); socket.on('refreshSheet', async () => { await updateBots(); sendBotData(); }); socket.on('disconnect', () => { clearInterval(updateInterval); console.log('Client disconnected'); }); }); // Serve static files app.use(express.static(__dirname)); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); // Start server const PORT = process.env.PORT || 7860; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); // Initial load and periodic updates updateBots(); setInterval(updateBots, 30000); // Check sheet every 30 seconds