| | 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 |
| |
|
| | |
| | log = logging.getLogger('werkzeug') |
| | log.setLevel(logging.ERROR) |
| | app = Flask(__name__) |
| |
|
| | |
| |
|
| | SHEET_ID = os.environ.get("SHEET_ID") |
| | SHEET_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv" |
| |
|
| | |
| | bots = {} |
| | bot_processes = {} |
| | last_rejoin = {} |
| | server_locks = {} |
| |
|
| | |
| | HTML = """<!DOCTYPE html> |
| | <html> |
| | <head> |
| | <title>Bot Manager</title> |
| | <meta charset="utf-8"> |
| | <meta name="viewport" content="width=device-width,initial-scale=1"> |
| | <style> |
| | *{margin:0;padding:0;box-sizing:border-box} |
| | body{font-family:Arial,sans-serif;background:#1a1a1a;color:#fff;padding:10px} |
| | .header{background:#2a2a2a;padding:15px;border-radius:8px;margin-bottom:15px} |
| | h1{font-size:24px;margin-bottom:10px} |
| | .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:15px} |
| | .stat{background:#333;padding:12px;border-radius:5px;text-align:center} |
| | .stat span{display:block;font-size:24px;font-weight:bold;color:#4CAF50;margin-top:5px} |
| | .controls{margin-bottom:15px} |
| | button{background:#4CAF50;border:none;color:white;padding:10px 20px;margin:5px;border-radius:5px;cursor:pointer;font-size:14px} |
| | button:hover{background:#45a049} |
| | button:disabled{background:#666;cursor:not-allowed} |
| | .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px} |
| | .bot{background:#2a2a2a;padding:15px;border-radius:8px;border-left:4px solid #666} |
| | .bot.online{border-color:#4CAF50} |
| | .bot.offline{border-color:#f44336} |
| | .bot.connecting{border-color:#ff9800} |
| | .bot-name{font-weight:bold;font-size:16px;margin-bottom:8px} |
| | .bot-info{color:#999;font-size:13px;margin:4px 0} |
| | .bot-status{display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;margin-top:8px;font-weight:bold} |
| | .online .bot-status{background:#4CAF50;color:white} |
| | .offline .bot-status{background:#f44336;color:white} |
| | .connecting .bot-status{background:#ff9800;color:white} |
| | .msg{background:#2196F3;color:white;padding:10px;border-radius:5px;margin:10px 0} |
| | .error{background:#f44336} |
| | </style> |
| | </head> |
| | <body> |
| | <div class="header"> |
| | <h1>🎮 Minecraft Bot Manager</h1> |
| | <div class="stats"> |
| | <div class="stat">Total<span id="total">0</span></div> |
| | <div class="stat">Online<span id="online">0</span></div> |
| | <div class="stat">Offline<span id="offline">0</span></div> |
| | <div class="stat">Connecting<span id="connecting">0</span></div> |
| | </div> |
| | <div class="controls"> |
| | <button onclick="reloadSheet()">📋 Reload Sheet</button> |
| | <button onclick="refreshStatus()">🔄 Refresh</button> |
| | </div> |
| | <div id="msg"></div> |
| | </div> |
| | <div class="grid" id="grid"></div> |
| | <script> |
| | let data={}; |
| | let updateTimer; |
| | |
| | async function fetchBots(){ |
| | try{ |
| | const r=await fetch('/api/bots'); |
| | data=await r.json(); |
| | renderBots(); |
| | }catch(e){ |
| | showMsg('Failed to fetch bots','error'); |
| | } |
| | } |
| | |
| | async function rejoinBot(name){ |
| | if(!confirm(`Rejoin bot ${name}?`))return; |
| | try{ |
| | const r=await fetch('/api/rejoin',{ |
| | method:'POST', |
| | headers:{'Content-Type':'application/json'}, |
| | body:JSON.stringify({name:name}) |
| | }); |
| | const res=await r.json(); |
| | if(res.error){ |
| | showMsg(res.error,'error'); |
| | }else{ |
| | showMsg(`Rejoining ${name}...`); |
| | fetchBots(); |
| | } |
| | }catch(e){ |
| | showMsg('Failed to rejoin bot','error'); |
| | } |
| | } |
| | |
| | async function reloadSheet(){ |
| | try{ |
| | showMsg('Reloading from sheet...'); |
| | await fetch('/api/reload',{method:'POST'}); |
| | setTimeout(fetchBots,1000); |
| | }catch(e){ |
| | showMsg('Failed to reload','error'); |
| | } |
| | } |
| | |
| | async function refreshStatus(){ |
| | fetchBots(); |
| | } |
| | |
| | function showMsg(text,type=''){ |
| | const el=document.getElementById('msg'); |
| | el.className=type; |
| | el.textContent=text; |
| | if(text)setTimeout(()=>el.textContent='',5000); |
| | } |
| | |
| | function renderBots(){ |
| | const g=document.getElementById('grid'); |
| | g.innerHTML=''; |
| | let total=0,online=0,offline=0,connecting=0; |
| | |
| | |
| | for(const[name,bot]of Object.entries(data)){ |
| | total++; |
| | if(bot.status==='online')online++; |
| | else if(bot.status==='offline')offline++; |
| | else connecting++; |
| | |
| | const div=document.createElement('div'); |
| | div.className='bot '+bot.status; |
| | let btnHtml=''; |
| | if(bot.status==='offline'){ |
| | if(bot.can_rejoin){ |
| | btnHtml=`<button onclick="rejoinBot('${name.replace(/'/g,"\\'")}')">🔄 Rejoin</button>`; |
| | }else if(bot.cooldown>0){ |
| | const m=Math.floor(bot.cooldown/60); |
| | const s=bot.cooldown%60; |
| | btnHtml=`<button disabled>⏳ ${m}:${s.toString().padStart(2,'0')}</button>`; |
| | } |
| | } |
| | div.innerHTML=` |
| | <div class="bot-name">${name}</div> |
| | <div class="bot-info">Version: ${bot.version}</div> |
| | <div class="bot-info">Server: ${bot.server||'Hidden'}</div> |
| | <div class="bot-status">${bot.status.toUpperCase()}</div> |
| | ${btnHtml}`; |
| | g.appendChild(div); |
| | } |
| | document.getElementById('total').textContent=total; |
| | document.getElementById('online').textContent=online; |
| | document.getElementById('offline').textContent=offline; |
| | document.getElementById('connecting').textContent=connecting; |
| | } |
| | |
| | function startAutoUpdate(){ |
| | if(updateTimer)clearInterval(updateTimer); |
| | updateTimer=setInterval(fetchBots,5000); |
| | } |
| | |
| | fetchBots(); |
| | startAutoUpdate(); |
| | </script> |
| | </body> |
| | </html>""" |
| |
|
| | |
| | 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): |
| | 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']}" |
| | |
| | |
| | if server_key in server_locks and server_locks[server_key] != name: |
| | return False, "Server already has another bot" |
| | |
| | |
| | if name in bot_processes: |
| | try: |
| | proc = bot_processes[name] |
| | proc.terminate() |
| | proc.wait(timeout=3) |
| | except: |
| | pass |
| | |
| | try: |
| | |
| | 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() |
| | } |
| | |
| | |
| | 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' |
| | |
| | |
| | if event in ['connected', 'disconnected', 'kicked', 'error']: |
| | print(f"[{name}] {event}: {data.get('reason', '')}") |
| | |
| | except json.JSONDecodeError: |
| | |
| | pass |
| | except: |
| | pass |
| | |
| | |
| | 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} |
| | |
| | |
| | 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}") |
| | |
| | |
| | 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', |
| | '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() |
| | |
| | |
| | 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) |
| | |
| | |
| | if not write_bot_script(): |
| | print("Failed to write bot script!") |
| | sys.exit(1) |
| | |
| | print("Bot Manager starting...") |
| | |
| | |
| | sync_bots() |
| | |
| | |
| | def auto_sync(): |
| | while True: |
| | time.sleep(60) |
| | try: |
| | sync_bots() |
| | except Exception as e: |
| | print(f"Auto sync error: {e}") |
| | |
| | threading.Thread(target=auto_sync, daemon=True).start() |
| | |
| | |
| | print("Server starting on port 7860...") |
| | app.run(host='0.0.0.0', port=7860, debug=False, use_reloader=False) |