MIneLine / app.py
Tokipo's picture
Update app.py
0b3254a verified
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 = """<!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 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)