|
|
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) |