Tokipo commited on
Commit
0b3254a
·
verified ·
1 Parent(s): 125b2d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +235 -150
app.py CHANGED
@@ -1,18 +1,23 @@
1
  import os
 
2
  import time
3
  import json
4
  import threading
5
  import subprocess
6
  import signal
7
- import sys
8
  import requests
9
- from flask import Flask, render_template_string, jsonify, request
10
  from datetime import datetime, timedelta
 
 
11
 
 
 
 
12
  app = Flask(__name__)
13
 
14
  # Configuration
15
- SHEET_ID = "109roJQr-Y4YCLTkCqaK6iwShC-Dr2Jb-hB0qE2phNqQ"
 
16
  SHEET_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv"
17
 
18
  # Storage
@@ -21,202 +26,271 @@ bot_processes = {}
21
  last_rejoin = {}
22
  server_locks = {}
23
 
24
- # Minimal HTML/CSS
25
  HTML = """<!DOCTYPE html>
26
  <html>
27
  <head>
28
  <title>Bot Manager</title>
 
29
  <meta name="viewport" content="width=device-width,initial-scale=1">
30
  <style>
31
  *{margin:0;padding:0;box-sizing:border-box}
32
- body{font-family:monospace;background:#222;color:#fff;padding:10px}
33
- .h{background:#333;padding:15px;margin-bottom:10px;border-radius:5px}
34
- h1{font-size:1.5em;margin-bottom:10px}
35
- .stats{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:15px}
36
- .stat{background:#444;padding:10px;border-radius:3px;flex:1;min-width:100px}
37
- .stat b{color:#0f0;display:block;font-size:1.5em}
38
- .btn{background:#555;border:none;color:#fff;padding:8px 15px;border-radius:3px;cursor:pointer;margin:5px}
39
- .btn:hover{background:#666}
40
- .btn:disabled{opacity:0.5;cursor:not-allowed}
41
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:10px}
42
- .bot{background:#333;padding:10px;border-radius:5px;border-left:3px solid #555}
43
- .bot.on{border-color:#0f0}
44
- .bot.off{border-color:#f00}
45
- .bot.wait{border-color:#fa0}
46
- .name{font-weight:bold;margin-bottom:5px}
47
- .info{font-size:0.9em;color:#aaa;margin:2px 0}
48
- .status{display:inline-block;padding:2px 8px;border-radius:2px;font-size:0.8em;margin:5px 0}
49
- .on .status{background:#0f0;color:#000}
50
- .off .status{background:#f00}
51
- .wait .status{background:#fa0;color:#000}
 
 
 
52
  </style>
53
  </head>
54
  <body>
55
- <div class="h">
56
  <h1>🎮 Minecraft Bot Manager</h1>
57
  <div class="stats">
58
- <div class="stat">Total<b id="total">0</b></div>
59
- <div class="stat">Online<b id="on">0</b></div>
60
- <div class="stat">Offline<b id="off">0</b></div>
 
61
  </div>
62
- <button class="btn" onclick="reload()">📋 Reload Sheet</button>
63
- <button class="btn" onclick="update()">🔄 Refresh</button>
 
 
 
64
  </div>
65
  <div class="grid" id="grid"></div>
66
  <script>
67
  let data={};
68
- async function update(){
 
 
69
  try{
70
  const r=await fetch('/api/bots');
71
  data=await r.json();
72
- render();
73
- }catch(e){console.error(e)}
 
74
  }
75
- async function rejoin(name){
 
 
 
76
  try{
77
  const r=await fetch('/api/rejoin',{
78
  method:'POST',
79
  headers:{'Content-Type':'application/json'},
80
  body:JSON.stringify({name:name})
81
  });
82
- const d=await r.json();
83
- if(d.error)alert(d.error);
84
- update();
85
- }catch(e){alert('Error: '+e)}
 
 
86
  }
87
- async function reload(){
 
 
 
 
 
 
 
88
  await fetch('/api/reload',{method:'POST'});
89
- update();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
- function render(){
 
92
  const g=document.getElementById('grid');
93
  g.innerHTML='';
94
- let t=0,on=0,off=0;
 
 
95
  for(const[name,bot]of Object.entries(data)){
96
- t++;
97
- if(bot.status=='online')on++;
98
- else off++;
 
 
99
  const div=document.createElement('div');
100
- div.className='bot '+(bot.status=='online'?'on':bot.status=='connecting'?'wait':'off');
101
- let btn='';
102
- if(bot.status=='offline'){
103
  if(bot.can_rejoin){
104
- btn=`<button class="btn" onclick="rejoin('${name}')">Join</button>`;
105
  }else if(bot.cooldown>0){
106
  const m=Math.floor(bot.cooldown/60);
107
  const s=bot.cooldown%60;
108
- btn=`<button class="btn" disabled>Wait ${m}:${s<10?'0':''}${s}</button>`;
109
  }
110
  }
111
  div.innerHTML=`
112
- <div class="name">${name}</div>
113
- <div class="info">Version: ${bot.version||'1.20.1'}</div>
114
- <div class="status">${bot.status.toUpperCase()}</div>
115
- ${btn}`;
 
116
  g.appendChild(div);
117
  }
118
- document.getElementById('total').textContent=t;
119
- document.getElementById('on').textContent=on;
120
- document.getElementById('off').textContent=off;
 
121
  }
122
- update();
123
- setInterval(update,3000);
 
 
 
 
 
 
124
  </script>
125
  </body>
126
  </html>"""
127
 
128
- # Bot JavaScript code
129
- BOT_CODE = """
130
  const mineflayer = require('mineflayer');
131
 
132
- const [botName, host, port, version] = process.argv.slice(2);
 
 
 
133
 
134
- console.log('STARTING', botName, host, port, version);
135
 
136
  const bot = mineflayer.createBot({
137
  host: host,
138
- port: parseInt(port) || 25565,
139
  username: botName,
140
- version: version || false,
141
  auth: 'offline',
 
142
  hideErrors: false,
143
- logErrors: true,
144
  checkTimeoutInterval: 30000,
145
- loadInternalPlugins: true,
146
- physicsEnabled: true,
147
- viewDistance: 'short'
148
  });
149
 
150
- let connected = false;
151
- let afkTimer = null;
152
 
153
  function startAfk() {
154
- if (afkTimer) clearInterval(afkTimer);
155
- afkTimer = setInterval(() => {
156
- if (!bot.entity) return;
 
 
157
 
158
  // Random movement
159
- const moves = ['forward', 'back', 'left', 'right', 'jump'];
160
- const move = moves[Math.floor(Math.random() * moves.length)];
161
 
162
- bot.setControlState(move, true);
163
- setTimeout(() => bot.setControlState(move, false), 100);
 
 
164
 
165
  // Random look
166
- if (Math.random() > 0.7) {
167
- bot.look(
168
- bot.entity.yaw + (Math.random() - 0.5) * 0.5,
169
- bot.entity.pitch + (Math.random() - 0.5) * 0.5
170
- );
171
  }
172
- }, 20000);
173
  }
174
 
175
  bot.once('spawn', () => {
176
- console.log('ONLINE');
177
- connected = true;
178
  startAfk();
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  });
180
 
181
  bot.on('respawn', () => {
182
- console.log('RESPAWNED');
183
  startAfk();
184
  });
185
 
186
  bot.on('death', () => {
187
- console.log('DIED');
188
  });
189
 
190
  bot.on('kicked', (reason) => {
191
- console.log('KICKED', reason);
192
- connected = false;
193
  });
194
 
195
  bot.on('error', (err) => {
196
- console.log('ERROR', err.message || err);
197
  });
198
 
199
  bot.on('end', (reason) => {
200
- console.log('OFFLINE', reason || '');
201
- if (afkTimer) clearInterval(afkTimer);
202
- connected = false;
203
  process.exit();
204
  });
205
 
206
- // Keep alive
207
  setInterval(() => {
208
- if (connected) console.log('ALIVE');
209
- }, 10000);
 
 
210
 
211
  // Graceful shutdown
212
  process.on('SIGTERM', () => {
213
- if (afkTimer) clearInterval(afkTimer);
214
  bot.quit();
215
  process.exit();
216
  });
217
 
218
  process.on('SIGINT', () => {
219
- if (afkTimer) clearInterval(afkTimer);
220
  bot.quit();
221
  process.exit();
222
  });
@@ -227,14 +301,14 @@ def write_bot_script():
227
  path = '/tmp/bot.js'
228
  try:
229
  with open(path, 'w') as f:
230
- f.write(BOT_CODE)
231
- return path
232
  except Exception as e:
233
  print(f"Error writing bot script: {e}")
234
- return None
235
 
236
- def get_sheet_data():
237
- """Fetch data from Google Sheets"""
238
  try:
239
  resp = requests.get(SHEET_URL, timeout=10)
240
  if resp.status_code != 200:
@@ -243,47 +317,48 @@ def get_sheet_data():
243
  lines = resp.text.strip().split('\n')
244
  data = []
245
 
246
- for line in lines[1:]: # Skip header
247
  parts = [p.strip().strip('"') for p in line.split(',')]
248
  if len(parts) >= 3:
249
  name = parts[0]
250
  ip = parts[1]
251
  port = parts[2]
252
- version = parts[3] if len(parts) > 3 else "1.20.1"
253
 
254
  if name and ip and port and port.isdigit():
255
  data.append({
256
  'name': name,
257
  'ip': ip,
258
  'port': port,
259
- 'version': version or "1.20.1"
260
  })
261
 
262
  print(f"Loaded {len(data)} bots from sheet")
263
  return data
264
  except Exception as e:
265
- print(f"Sheet error: {e}")
266
  return []
267
 
268
  def start_bot(config):
269
  """Start a bot process"""
270
  name = config['name']
271
- server = f"{config['ip']}:{config['port']}"
272
 
273
  # Check if server already has a bot
274
- if server in server_locks and server_locks[server] != name:
275
- return False, "Server already has a bot"
276
 
277
- # Kill existing process
278
  if name in bot_processes:
279
  try:
280
- bot_processes[name].terminate()
281
- bot_processes[name].wait(timeout=3)
 
282
  except:
283
  pass
284
 
285
  try:
286
- # Start bot process
287
  proc = subprocess.Popen(
288
  ['node', '/tmp/bot.js', name, config['ip'], config['port'], config['version']],
289
  stdout=subprocess.PIPE,
@@ -293,25 +368,26 @@ def start_bot(config):
293
  )
294
 
295
  bot_processes[name] = proc
296
- server_locks[server] = name
297
 
298
  bots[name] = {
299
  'status': 'connecting',
300
  'ip': config['ip'],
301
  'port': config['port'],
302
  'version': config['version'],
303
- 'server': server
 
304
  }
305
 
306
- # Start monitor thread
307
  threading.Thread(target=monitor_bot, args=(name,), daemon=True).start()
308
 
309
- return True, "Started"
310
  except Exception as e:
311
  return False, str(e)
312
 
313
  def monitor_bot(name):
314
- """Monitor bot output"""
315
  if name not in bot_processes:
316
  return
317
 
@@ -322,19 +398,25 @@ def monitor_bot(name):
322
  line = proc.stdout.readline()
323
  if not line:
324
  continue
 
 
 
 
325
 
326
- line = line.strip()
327
- if not line:
328
- continue
 
 
 
329
 
330
- print(f"[{name}] {line}")
331
-
332
- if 'ONLINE' in line or 'ALIVE' in line:
333
- bots[name]['status'] = 'online'
334
- elif 'OFFLINE' in line or 'KICKED' in line or 'ERROR' in line:
335
- bots[name]['status'] = 'offline'
336
- elif 'DIED' in line:
337
- bots[name]['status'] = 'online' # Still online, just dead
338
  except:
339
  pass
340
 
@@ -347,7 +429,7 @@ def monitor_bot(name):
347
 
348
  def sync_bots():
349
  """Sync bots with sheet data"""
350
- data = get_sheet_data()
351
  names = {d['name'] for d in data}
352
 
353
  # Remove deleted bots
@@ -360,14 +442,17 @@ def sync_bots():
360
  pass
361
  if name in bots:
362
  server = bots[name].get('server')
363
- if server in server_locks:
364
  del server_locks[server]
365
  del bots[name]
 
366
 
367
  # Add new bots
368
  for config in data:
369
  if config['name'] not in bots:
370
- start_bot(config)
 
 
371
 
372
  @app.route('/')
373
  def index():
@@ -392,6 +477,7 @@ def api_bots():
392
  result[name] = {
393
  'status': bot['status'],
394
  'version': bot['version'],
 
395
  'can_rejoin': can_rejoin,
396
  'cooldown': cooldown
397
  }
@@ -413,10 +499,10 @@ def api_rejoin():
413
  elapsed = now - last_rejoin[name]
414
  if elapsed < timedelta(hours=1):
415
  mins = int((timedelta(hours=1) - elapsed).total_seconds() / 60)
416
- return jsonify({'error': f'Wait {mins} minutes'}), 429
417
 
418
  if bots[name]['status'] == 'online':
419
- return jsonify({'error': 'Already online'}), 400
420
 
421
  config = {
422
  'name': name,
@@ -429,20 +515,20 @@ def api_rejoin():
429
 
430
  if success:
431
  last_rejoin[name] = now
432
- return jsonify({'success': True})
433
  else:
434
  return jsonify({'error': msg}), 500
435
 
436
  @app.route('/api/reload', methods=['POST'])
437
  def api_reload():
438
- """Reload from sheet"""
439
  sync_bots()
440
- return jsonify({'success': True})
441
 
442
  def cleanup(sig=None, frame=None):
443
- """Cleanup on exit"""
444
  print("\nShutting down...")
445
- for proc in bot_processes.values():
446
  try:
447
  proc.terminate()
448
  except:
@@ -454,27 +540,26 @@ if __name__ == '__main__':
454
  signal.signal(signal.SIGTERM, cleanup)
455
 
456
  # Setup
457
- script_path = write_bot_script()
458
- if not script_path:
459
  print("Failed to write bot script!")
460
  sys.exit(1)
461
 
462
- print("Bot script ready")
463
 
464
  # Initial sync
465
  sync_bots()
466
 
467
- # Periodic sync
468
  def auto_sync():
469
  while True:
470
- time.sleep(60)
471
  try:
472
  sync_bots()
473
  except Exception as e:
474
- print(f"Sync error: {e}")
475
 
476
  threading.Thread(target=auto_sync, daemon=True).start()
477
 
478
- # Start server
479
- print("Starting server on port 7860...")
480
- app.run(host='0.0.0.0', port=7860, debug=False)
 
1
  import os
2
+ import sys
3
  import time
4
  import json
5
  import threading
6
  import subprocess
7
  import signal
 
8
  import requests
 
9
  from datetime import datetime, timedelta
10
+ from flask import Flask, render_template_string, jsonify, request
11
+ import logging
12
 
13
+ # Disable Flask logging
14
+ log = logging.getLogger('werkzeug')
15
+ log.setLevel(logging.ERROR)
16
  app = Flask(__name__)
17
 
18
  # Configuration
19
+
20
+ SHEET_ID = os.environ.get("SHEET_ID")
21
  SHEET_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv"
22
 
23
  # Storage
 
26
  last_rejoin = {}
27
  server_locks = {}
28
 
29
+ # HTML Interface
30
  HTML = """<!DOCTYPE html>
31
  <html>
32
  <head>
33
  <title>Bot Manager</title>
34
+ <meta charset="utf-8">
35
  <meta name="viewport" content="width=device-width,initial-scale=1">
36
  <style>
37
  *{margin:0;padding:0;box-sizing:border-box}
38
+ body{font-family:Arial,sans-serif;background:#1a1a1a;color:#fff;padding:10px}
39
+ .header{background:#2a2a2a;padding:15px;border-radius:8px;margin-bottom:15px}
40
+ h1{font-size:24px;margin-bottom:10px}
41
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:15px}
42
+ .stat{background:#333;padding:12px;border-radius:5px;text-align:center}
43
+ .stat span{display:block;font-size:24px;font-weight:bold;color:#4CAF50;margin-top:5px}
44
+ .controls{margin-bottom:15px}
45
+ button{background:#4CAF50;border:none;color:white;padding:10px 20px;margin:5px;border-radius:5px;cursor:pointer;font-size:14px}
46
+ button:hover{background:#45a049}
47
+ button:disabled{background:#666;cursor:not-allowed}
48
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
49
+ .bot{background:#2a2a2a;padding:15px;border-radius:8px;border-left:4px solid #666}
50
+ .bot.online{border-color:#4CAF50}
51
+ .bot.offline{border-color:#f44336}
52
+ .bot.connecting{border-color:#ff9800}
53
+ .bot-name{font-weight:bold;font-size:16px;margin-bottom:8px}
54
+ .bot-info{color:#999;font-size:13px;margin:4px 0}
55
+ .bot-status{display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;margin-top:8px;font-weight:bold}
56
+ .online .bot-status{background:#4CAF50;color:white}
57
+ .offline .bot-status{background:#f44336;color:white}
58
+ .connecting .bot-status{background:#ff9800;color:white}
59
+ .msg{background:#2196F3;color:white;padding:10px;border-radius:5px;margin:10px 0}
60
+ .error{background:#f44336}
61
  </style>
62
  </head>
63
  <body>
64
+ <div class="header">
65
  <h1>🎮 Minecraft Bot Manager</h1>
66
  <div class="stats">
67
+ <div class="stat">Total<span id="total">0</span></div>
68
+ <div class="stat">Online<span id="online">0</span></div>
69
+ <div class="stat">Offline<span id="offline">0</span></div>
70
+ <div class="stat">Connecting<span id="connecting">0</span></div>
71
  </div>
72
+ <div class="controls">
73
+ <button onclick="reloadSheet()">📋 Reload Sheet</button>
74
+ <button onclick="refreshStatus()">🔄 Refresh</button>
75
+ </div>
76
+ <div id="msg"></div>
77
  </div>
78
  <div class="grid" id="grid"></div>
79
  <script>
80
  let data={};
81
+ let updateTimer;
82
+
83
+ async function fetchBots(){
84
  try{
85
  const r=await fetch('/api/bots');
86
  data=await r.json();
87
+ renderBots();
88
+ }catch(e){
89
+ showMsg('Failed to fetch bots','error');
90
  }
91
+ }
92
+
93
+ async function rejoinBot(name){
94
+ if(!confirm(`Rejoin bot ${name}?`))return;
95
  try{
96
  const r=await fetch('/api/rejoin',{
97
  method:'POST',
98
  headers:{'Content-Type':'application/json'},
99
  body:JSON.stringify({name:name})
100
  });
101
+ const res=await r.json();
102
+ if(res.error){
103
+ showMsg(res.error,'error');
104
+ }else{
105
+ showMsg(`Rejoining ${name}...`);
106
+ fetchBots();
107
  }
108
+ }catch(e){
109
+ showMsg('Failed to rejoin bot','error');
110
+ }
111
+ }
112
+
113
+ async function reloadSheet(){
114
+ try{
115
+ showMsg('Reloading from sheet...');
116
  await fetch('/api/reload',{method:'POST'});
117
+ setTimeout(fetchBots,1000);
118
+ }catch(e){
119
+ showMsg('Failed to reload','error');
120
+ }
121
+ }
122
+
123
+ async function refreshStatus(){
124
+ fetchBots();
125
+ }
126
+
127
+ function showMsg(text,type=''){
128
+ const el=document.getElementById('msg');
129
+ el.className=type;
130
+ el.textContent=text;
131
+ if(text)setTimeout(()=>el.textContent='',5000);
132
  }
133
+
134
+ function renderBots(){
135
  const g=document.getElementById('grid');
136
  g.innerHTML='';
137
+ let total=0,online=0,offline=0,connecting=0;
138
+
139
+
140
  for(const[name,bot]of Object.entries(data)){
141
+ total++;
142
+ if(bot.status==='online')online++;
143
+ else if(bot.status==='offline')offline++;
144
+ else connecting++;
145
+
146
  const div=document.createElement('div');
147
+ div.className='bot '+bot.status;
148
+ let btnHtml='';
149
+ if(bot.status==='offline'){
150
  if(bot.can_rejoin){
151
+ btnHtml=`<button onclick="rejoinBot('${name.replace(/'/g,"\\'")}')">🔄 Rejoin</button>`;
152
  }else if(bot.cooldown>0){
153
  const m=Math.floor(bot.cooldown/60);
154
  const s=bot.cooldown%60;
155
+ btnHtml=`<button disabled>⏳ ${m}:${s.toString().padStart(2,'0')}</button>`;
156
  }
157
  }
158
  div.innerHTML=`
159
+ <div class="bot-name">${name}</div>
160
+ <div class="bot-info">Version: ${bot.version}</div>
161
+ <div class="bot-info">Server: ${bot.server||'Hidden'}</div>
162
+ <div class="bot-status">${bot.status.toUpperCase()}</div>
163
+ ${btnHtml}`;
164
  g.appendChild(div);
165
  }
166
+ document.getElementById('total').textContent=total;
167
+ document.getElementById('online').textContent=online;
168
+ document.getElementById('offline').textContent=offline;
169
+ document.getElementById('connecting').textContent=connecting;
170
  }
171
+
172
+ function startAutoUpdate(){
173
+ if(updateTimer)clearInterval(updateTimer);
174
+ updateTimer=setInterval(fetchBots,5000);
175
+ }
176
+
177
+ fetchBots();
178
+ startAutoUpdate();
179
  </script>
180
  </body>
181
  </html>"""
182
 
183
+ # Bot Node.js script
184
+ BOT_SCRIPT = """
185
  const mineflayer = require('mineflayer');
186
 
187
+ const botName = process.argv[2];
188
+ const host = process.argv[3];
189
+ const port = parseInt(process.argv[4]) || 25565;
190
+ const version = process.argv[5] || false;
191
 
192
+ console.log(JSON.stringify({event:'starting',name:botName,host:host,port:port,version:version}));
193
 
194
  const bot = mineflayer.createBot({
195
  host: host,
196
+ port: port,
197
  username: botName,
 
198
  auth: 'offline',
199
+ version: version === 'false' ? false : version,
200
  hideErrors: false,
 
201
  checkTimeoutInterval: 30000,
202
+ keepAlive: true,
203
+ skipValidation: true
 
204
  });
205
 
206
+ let isConnected = false;
207
+ let afkInterval = null;
208
 
209
  function startAfk() {
210
+ if (afkInterval) clearInterval(afkInterval);
211
+
212
+ // Anti-AFK movement
213
+ afkInterval = setInterval(() => {
214
+ if (!bot.entity || !isConnected) return;
215
 
216
  // Random movement
217
+ const actions = ['forward', 'back', 'left', 'right', 'jump', 'sneak'];
218
+ const action = actions[Math.floor(Math.random() * actions.length)];
219
 
220
+ bot.setControlState(action, true);
221
+ setTimeout(() => {
222
+ bot.setControlState(action, false);
223
+ }, 200);
224
 
225
  // Random look
226
+ if (Math.random() > 0.5) {
227
+ const yaw = bot.entity.yaw + (Math.random() - 0.5);
228
+ const pitch = bot.entity.pitch + (Math.random() - 0.5) * 0.5;
229
+ bot.look(yaw, pitch, true);
 
230
  }
231
+ }, 15000); // Move every 15 seconds
232
  }
233
 
234
  bot.once('spawn', () => {
235
+ console.log(JSON.stringify({event:'connected',name:botName}));
236
+ isConnected = true;
237
  startAfk();
238
+
239
+ // Handle resource packs
240
+ bot._client.on('resource_pack_send', (packet) => {
241
+ bot._client.write('resource_pack_receive', { result: 0 });
242
+ });
243
+
244
+ // Random chat to stay active
245
+ setInterval(() => {
246
+ if (isConnected && Math.random() > 0.95) {
247
+ const messages = ['hi', 'hello', 'hey', ':)', 'o/', 'test'];
248
+ bot.chat(messages[Math.floor(Math.random() * messages.length)]);
249
+ }
250
+ }, 300000); // Every 5 minutes maybe chat
251
  });
252
 
253
  bot.on('respawn', () => {
254
+ console.log(JSON.stringify({event:'respawned',name:botName}));
255
  startAfk();
256
  });
257
 
258
  bot.on('death', () => {
259
+ console.log(JSON.stringify({event:'died',name:botName}));
260
  });
261
 
262
  bot.on('kicked', (reason) => {
263
+ console.log(JSON.stringify({event:'kicked',name:botName,reason:reason}));
264
+ isConnected = false;
265
  });
266
 
267
  bot.on('error', (err) => {
268
+ console.log(JSON.stringify({event:'error',name:botName,error:err.message}));
269
  });
270
 
271
  bot.on('end', (reason) => {
272
+ console.log(JSON.stringify({event:'disconnected',name:botName,reason:reason}));
273
+ isConnected = false;
274
+ if (afkInterval) clearInterval(afkInterval);
275
  process.exit();
276
  });
277
 
278
+ // Keep alive signal
279
  setInterval(() => {
280
+ if (isConnected) {
281
+ console.log(JSON.stringify({event:'alive',name:botName}));
282
+ }
283
+ }, 30000);
284
 
285
  // Graceful shutdown
286
  process.on('SIGTERM', () => {
287
+ if (afkInterval) clearInterval(afkInterval);
288
  bot.quit();
289
  process.exit();
290
  });
291
 
292
  process.on('SIGINT', () => {
293
+ if (afkInterval) clearInterval(afkInterval);
294
  bot.quit();
295
  process.exit();
296
  });
 
301
  path = '/tmp/bot.js'
302
  try:
303
  with open(path, 'w') as f:
304
+ f.write(BOT_SCRIPT)
305
+ return True
306
  except Exception as e:
307
  print(f"Error writing bot script: {e}")
308
+ return False
309
 
310
+ def fetch_sheet_data():
311
+ """Fetch bot configuration from Google Sheets"""
312
  try:
313
  resp = requests.get(SHEET_URL, timeout=10)
314
  if resp.status_code != 200:
 
317
  lines = resp.text.strip().split('\n')
318
  data = []
319
 
320
+ for i, line in enumerate(lines[1:], 1): # Skip header
321
  parts = [p.strip().strip('"') for p in line.split(',')]
322
  if len(parts) >= 3:
323
  name = parts[0]
324
  ip = parts[1]
325
  port = parts[2]
326
+ version = parts[3] if len(parts) > 3 else "false"
327
 
328
  if name and ip and port and port.isdigit():
329
  data.append({
330
  'name': name,
331
  'ip': ip,
332
  'port': port,
333
+ 'version': version if version else "false"
334
  })
335
 
336
  print(f"Loaded {len(data)} bots from sheet")
337
  return data
338
  except Exception as e:
339
+ print(f"Error fetching sheet: {e}")
340
  return []
341
 
342
  def start_bot(config):
343
  """Start a bot process"""
344
  name = config['name']
345
+ server_key = f"{config['ip']}:{config['port']}"
346
 
347
  # Check if server already has a bot
348
+ if server_key in server_locks and server_locks[server_key] != name:
349
+ return False, "Server already has another bot"
350
 
351
+ # Kill existing process if any
352
  if name in bot_processes:
353
  try:
354
+ proc = bot_processes[name]
355
+ proc.terminate()
356
+ proc.wait(timeout=3)
357
  except:
358
  pass
359
 
360
  try:
361
+ # Start new bot process
362
  proc = subprocess.Popen(
363
  ['node', '/tmp/bot.js', name, config['ip'], config['port'], config['version']],
364
  stdout=subprocess.PIPE,
 
368
  )
369
 
370
  bot_processes[name] = proc
371
+ server_locks[server_key] = name
372
 
373
  bots[name] = {
374
  'status': 'connecting',
375
  'ip': config['ip'],
376
  'port': config['port'],
377
  'version': config['version'],
378
+ 'server': server_key,
379
+ 'start_time': time.time()
380
  }
381
 
382
+ # Start monitoring thread
383
  threading.Thread(target=monitor_bot, args=(name,), daemon=True).start()
384
 
385
+ return True, "Bot started"
386
  except Exception as e:
387
  return False, str(e)
388
 
389
  def monitor_bot(name):
390
+ """Monitor bot process output"""
391
  if name not in bot_processes:
392
  return
393
 
 
398
  line = proc.stdout.readline()
399
  if not line:
400
  continue
401
+
402
+ try:
403
+ data = json.loads(line.strip())
404
+ event = data.get('event')
405
 
406
+ if event == 'connected' or event == 'alive':
407
+ if name in bots:
408
+ bots[name]['status'] = 'online'
409
+ elif event in ['disconnected', 'kicked', 'error']:
410
+ if name in bots:
411
+ bots[name]['status'] = 'offline'
412
 
413
+ # Only log important events
414
+ if event in ['connected', 'disconnected', 'kicked', 'error']:
415
+ print(f"[{name}] {event}: {data.get('reason', '')}")
416
+
417
+ except json.JSONDecodeError:
418
+ # Not JSON, ignore
419
+ pass
 
420
  except:
421
  pass
422
 
 
429
 
430
  def sync_bots():
431
  """Sync bots with sheet data"""
432
+ data = fetch_sheet_data()
433
  names = {d['name'] for d in data}
434
 
435
  # Remove deleted bots
 
442
  pass
443
  if name in bots:
444
  server = bots[name].get('server')
445
+ if server in server_locks and server_locks[server] == name:
446
  del server_locks[server]
447
  del bots[name]
448
+ print(f"Removed bot: {name}")
449
 
450
  # Add new bots
451
  for config in data:
452
  if config['name'] not in bots:
453
+ success, msg = start_bot(config)
454
+ if success:
455
+ print(f"Added bot: {config['name']}")
456
 
457
  @app.route('/')
458
  def index():
 
477
  result[name] = {
478
  'status': bot['status'],
479
  'version': bot['version'],
480
+ 'server': 'Hidden', # Hide server info
481
  'can_rejoin': can_rejoin,
482
  'cooldown': cooldown
483
  }
 
499
  elapsed = now - last_rejoin[name]
500
  if elapsed < timedelta(hours=1):
501
  mins = int((timedelta(hours=1) - elapsed).total_seconds() / 60)
502
+ return jsonify({'error': f'Please wait {mins} minutes before rejoining'}), 429
503
 
504
  if bots[name]['status'] == 'online':
505
+ return jsonify({'error': 'Bot is already online'}), 400
506
 
507
  config = {
508
  'name': name,
 
515
 
516
  if success:
517
  last_rejoin[name] = now
518
+ return jsonify({'success': True, 'message': 'Bot rejoining'})
519
  else:
520
  return jsonify({'error': msg}), 500
521
 
522
  @app.route('/api/reload', methods=['POST'])
523
  def api_reload():
524
+ """Reload bots from sheet"""
525
  sync_bots()
526
+ return jsonify({'success': True, 'message': 'Reloaded from sheet'})
527
 
528
  def cleanup(sig=None, frame=None):
529
+ """Clean shutdown"""
530
  print("\nShutting down...")
531
+ for name, proc in bot_processes.items():
532
  try:
533
  proc.terminate()
534
  except:
 
540
  signal.signal(signal.SIGTERM, cleanup)
541
 
542
  # Setup
543
+ if not write_bot_script():
 
544
  print("Failed to write bot script!")
545
  sys.exit(1)
546
 
547
+ print("Bot Manager starting...")
548
 
549
  # Initial sync
550
  sync_bots()
551
 
552
+ # Periodic sync with sheet
553
  def auto_sync():
554
  while True:
555
+ time.sleep(60) # Check every minute
556
  try:
557
  sync_bots()
558
  except Exception as e:
559
+ print(f"Auto sync error: {e}")
560
 
561
  threading.Thread(target=auto_sync, daemon=True).start()
562
 
563
+ # Start Flask server
564
+ print("Server starting on port 7860...")
565
+ app.run(host='0.0.0.0', port=7860, debug=False, use_reloader=False)