import os import sys import time import json import threading import subprocess import signal import requests from datetime import datetime, timedelta from flask import Flask, render_template_string, jsonify, request import logging # Disable Flask logging log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) app = Flask(__name__) # Configuration SHEET_ID = os.environ.get("SHEET_ID") SHEET_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv" # Storage bots = {} bot_processes = {} last_rejoin = {} server_locks = {} # HTML Interface HTML = """ Bot Manager

🎮 Minecraft Bot Manager

Total0
Online0
Offline0
Connecting0
""" # Bot Node.js script BOT_SCRIPT = """ const mineflayer = require('mineflayer'); const botName = process.argv[2]; const host = process.argv[3]; const port = parseInt(process.argv[4]) || 25565; const version = process.argv[5] || false; console.log(JSON.stringify({event:'starting',name:botName,host:host,port:port,version:version})); const bot = mineflayer.createBot({ host: host, port: port, username: botName, auth: 'offline', version: version === 'false' ? false : version, hideErrors: false, checkTimeoutInterval: 30000, keepAlive: true, skipValidation: true }); let isConnected = false; let afkInterval = null; function startAfk() { if (afkInterval) clearInterval(afkInterval); // Anti-AFK movement afkInterval = setInterval(() => { if (!bot.entity || !isConnected) return; // Random movement const actions = ['forward', 'back', 'left', 'right', 'jump', 'sneak']; const action = actions[Math.floor(Math.random() * actions.length)]; bot.setControlState(action, true); setTimeout(() => { bot.setControlState(action, false); }, 200); // Random look if (Math.random() > 0.5) { const yaw = bot.entity.yaw + (Math.random() - 0.5); const pitch = bot.entity.pitch + (Math.random() - 0.5) * 0.5; bot.look(yaw, pitch, true); } }, 15000); // Move every 15 seconds } bot.once('spawn', () => { console.log(JSON.stringify({event:'connected',name:botName})); isConnected = true; startAfk(); // Handle resource packs bot._client.on('resource_pack_send', (packet) => { bot._client.write('resource_pack_receive', { result: 0 }); }); // Random chat to stay active setInterval(() => { if (isConnected && Math.random() > 0.95) { const messages = ['hi', 'hello', 'hey', ':)', 'o/', 'test']; bot.chat(messages[Math.floor(Math.random() * messages.length)]); } }, 300000); // Every 5 minutes maybe chat }); bot.on('respawn', () => { console.log(JSON.stringify({event:'respawned',name:botName})); startAfk(); }); bot.on('death', () => { console.log(JSON.stringify({event:'died',name:botName})); }); bot.on('kicked', (reason) => { console.log(JSON.stringify({event:'kicked',name:botName,reason:reason})); isConnected = false; }); bot.on('error', (err) => { console.log(JSON.stringify({event:'error',name:botName,error:err.message})); }); bot.on('end', (reason) => { console.log(JSON.stringify({event:'disconnected',name:botName,reason:reason})); isConnected = false; if (afkInterval) clearInterval(afkInterval); process.exit(); }); // Keep alive signal setInterval(() => { if (isConnected) { console.log(JSON.stringify({event:'alive',name:botName})); } }, 30000); // Graceful shutdown process.on('SIGTERM', () => { if (afkInterval) clearInterval(afkInterval); bot.quit(); process.exit(); }); process.on('SIGINT', () => { if (afkInterval) clearInterval(afkInterval); bot.quit(); process.exit(); }); """ def write_bot_script(): """Write bot script to temp directory""" path = '/tmp/bot.js' try: with open(path, 'w') as f: f.write(BOT_SCRIPT) return True except Exception as e: print(f"Error writing bot script: {e}") return False def fetch_sheet_data(): """Fetch bot configuration from Google Sheets""" try: resp = requests.get(SHEET_URL, timeout=10) if resp.status_code != 200: return [] lines = resp.text.strip().split('\n') data = [] for i, line in enumerate(lines[1:], 1): # Skip header parts = [p.strip().strip('"') for p in line.split(',')] if len(parts) >= 3: name = parts[0] ip = parts[1] port = parts[2] version = parts[3] if len(parts) > 3 else "false" if name and ip and port and port.isdigit(): data.append({ 'name': name, 'ip': ip, 'port': port, 'version': version if version else "false" }) print(f"Loaded {len(data)} bots from sheet") return data except Exception as e: print(f"Error fetching sheet: {e}") return [] def start_bot(config): """Start a bot process""" name = config['name'] server_key = f"{config['ip']}:{config['port']}" # Check if server already has a bot if server_key in server_locks and server_locks[server_key] != name: return False, "Server already has another bot" # Kill existing process if any if name in bot_processes: try: proc = bot_processes[name] proc.terminate() proc.wait(timeout=3) except: pass try: # Start new bot process proc = subprocess.Popen( ['node', '/tmp/bot.js', name, config['ip'], config['port'], config['version']], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) bot_processes[name] = proc server_locks[server_key] = name bots[name] = { 'status': 'connecting', 'ip': config['ip'], 'port': config['port'], 'version': config['version'], 'server': server_key, 'start_time': time.time() } # Start monitoring thread threading.Thread(target=monitor_bot, args=(name,), daemon=True).start() return True, "Bot started" except Exception as e: return False, str(e) def monitor_bot(name): """Monitor bot process output""" if name not in bot_processes: return proc = bot_processes[name] try: while proc.poll() is None: line = proc.stdout.readline() if not line: continue try: data = json.loads(line.strip()) event = data.get('event') if event == 'connected' or event == 'alive': if name in bots: bots[name]['status'] = 'online' elif event in ['disconnected', 'kicked', 'error']: if name in bots: bots[name]['status'] = 'offline' # Only log important events if event in ['connected', 'disconnected', 'kicked', 'error']: print(f"[{name}] {event}: {data.get('reason', '')}") except json.JSONDecodeError: # Not JSON, ignore pass except: pass # Process ended if name in bots: bots[name]['status'] = 'offline' server = bots[name].get('server') if server in server_locks and server_locks[server] == name: del server_locks[server] def sync_bots(): """Sync bots with sheet data""" data = fetch_sheet_data() names = {d['name'] for d in data} # Remove deleted bots for name in list(bots.keys()): if name not in names: if name in bot_processes: try: bot_processes[name].terminate() except: pass if name in bots: server = bots[name].get('server') if server in server_locks and server_locks[server] == name: del server_locks[server] del bots[name] print(f"Removed bot: {name}") # Add new bots for config in data: if config['name'] not in bots: success, msg = start_bot(config) if success: print(f"Added bot: {config['name']}") @app.route('/') def index(): return HTML @app.route('/api/bots') def api_bots(): """Get bot status""" result = {} now = datetime.now() for name, bot in bots.items(): can_rejoin = True cooldown = 0 if name in last_rejoin: elapsed = now - last_rejoin[name] if elapsed < timedelta(hours=1): can_rejoin = False cooldown = int((timedelta(hours=1) - elapsed).total_seconds()) result[name] = { 'status': bot['status'], 'version': bot['version'], 'server': 'Hidden', # Hide server info 'can_rejoin': can_rejoin, 'cooldown': cooldown } return jsonify(result) @app.route('/api/rejoin', methods=['POST']) def api_rejoin(): """Rejoin a bot""" name = request.json.get('name') if name not in bots: return jsonify({'error': 'Bot not found'}), 404 now = datetime.now() # Check cooldown if name in last_rejoin: elapsed = now - last_rejoin[name] if elapsed < timedelta(hours=1): mins = int((timedelta(hours=1) - elapsed).total_seconds() / 60) return jsonify({'error': f'Please wait {mins} minutes before rejoining'}), 429 if bots[name]['status'] == 'online': return jsonify({'error': 'Bot is already online'}), 400 config = { 'name': name, 'ip': bots[name]['ip'], 'port': bots[name]['port'], 'version': bots[name]['version'] } success, msg = start_bot(config) if success: last_rejoin[name] = now return jsonify({'success': True, 'message': 'Bot rejoining'}) else: return jsonify({'error': msg}), 500 @app.route('/api/reload', methods=['POST']) def api_reload(): """Reload bots from sheet""" sync_bots() return jsonify({'success': True, 'message': 'Reloaded from sheet'}) def cleanup(sig=None, frame=None): """Clean shutdown""" print("\nShutting down...") for name, proc in bot_processes.items(): try: proc.terminate() except: pass sys.exit(0) if __name__ == '__main__': signal.signal(signal.SIGINT, cleanup) signal.signal(signal.SIGTERM, cleanup) # Setup if not write_bot_script(): print("Failed to write bot script!") sys.exit(1) print("Bot Manager starting...") # Initial sync sync_bots() # Periodic sync with sheet def auto_sync(): while True: time.sleep(60) # Check every minute try: sync_bots() except Exception as e: print(f"Auto sync error: {e}") threading.Thread(target=auto_sync, daemon=True).start() # Start Flask server print("Server starting on port 7860...") app.run(host='0.0.0.0', port=7860, debug=False, use_reloader=False)