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