Tokipo commited on
Commit
7907878
·
verified ·
1 Parent(s): 78662ef

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +349 -615
app.py CHANGED
@@ -1,746 +1,480 @@
1
  import os
2
- import json
3
  import time
 
4
  import threading
5
- import requests
6
- from flask import Flask, render_template_string, jsonify, request
7
- from datetime import datetime, timedelta
8
  import subprocess
9
  import signal
10
  import sys
 
 
 
11
 
12
  app = Flask(__name__)
13
 
14
- # Working directory for bot files
15
- WORK_DIR = "/tmp"
16
- BOT_SCRIPT_PATH = os.path.join(WORK_DIR, "bot.js")
17
-
18
- # Google Sheets 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
- # Bot storage
24
  bots = {}
25
  bot_processes = {}
26
- last_rejoin_times = {}
27
- server_bots = {} # Track one bot per server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- # Node.js bot script
30
- BOT_SCRIPT = """
31
  const mineflayer = require('mineflayer');
32
- const args = process.argv.slice(2);
33
 
34
- const botName = args[0];
35
- const host = args[1];
36
- const port = parseInt(args[2]);
37
- const version = args[3] || '1.20.1';
38
 
39
- console.log(`Starting bot ${botName} for ${host}:${port}`);
40
 
41
  const bot = mineflayer.createBot({
42
  host: host,
43
- port: port,
44
  username: botName,
45
- version: version,
46
- hideErrors: true,
47
- checkTimeoutInterval: 60000,
48
- auth: 'offline'
 
 
 
 
49
  });
50
 
51
- let afkInterval;
 
52
 
53
- bot.on('spawn', () => {
54
- console.log('CONNECTED');
55
-
56
- // Anti-AFK system
57
- afkInterval = setInterval(() => {
58
- const actions = [
59
- () => {
60
- bot.setControlState('forward', true);
61
- setTimeout(() => bot.setControlState('forward', false), 100);
62
- },
63
- () => {
64
- bot.setControlState('back', true);
65
- setTimeout(() => bot.setControlState('back', false), 100);
66
- },
67
- () => {
68
- bot.setControlState('left', true);
69
- setTimeout(() => bot.setControlState('left', false), 100);
70
- },
71
- () => {
72
- bot.setControlState('right', true);
73
- setTimeout(() => bot.setControlState('right', false), 100);
74
- },
75
- () => {
76
- bot.setControlState('jump', true);
77
- setTimeout(() => bot.setControlState('jump', false), 100);
78
- },
79
- () => {
80
- bot.setControlState('sneak', true);
81
- setTimeout(() => bot.setControlState('sneak', false), 100);
82
- }
83
- ];
84
 
85
- const randomAction = actions[Math.floor(Math.random() * actions.length)];
86
- randomAction();
87
- }, 30000); // Move every 30 seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  });
89
 
90
  bot.on('death', () => {
91
  console.log('DIED');
92
- setTimeout(() => {
93
- if (bot.health !== undefined) {
94
- bot.respawn();
95
- }
96
- }, 5000);
97
  });
98
 
99
  bot.on('kicked', (reason) => {
100
- console.log('KICKED:', reason);
101
- if (afkInterval) clearInterval(afkInterval);
102
  });
103
 
104
  bot.on('error', (err) => {
105
- console.log('ERROR:', err.message);
106
  });
107
 
108
- bot.on('end', () => {
109
- console.log('DISCONNECTED');
110
- if (afkInterval) clearInterval(afkInterval);
 
111
  process.exit();
112
  });
113
 
114
- // Handle process termination
 
 
 
 
 
115
  process.on('SIGTERM', () => {
116
- if (afkInterval) clearInterval(afkInterval);
117
  bot.quit();
118
  process.exit();
119
  });
120
- """
121
 
122
- HTML_TEMPLATE = """
123
- <!DOCTYPE html>
124
- <html>
125
- <head>
126
- <title>Mineflayer Bot Manager</title>
127
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
128
- <style>
129
- * {
130
- margin: 0;
131
- padding: 0;
132
- box-sizing: border-box;
133
- }
134
- body {
135
- font-family: 'Segoe UI', Arial, sans-serif;
136
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
137
- min-height: 100vh;
138
- padding: 20px;
139
- }
140
- .container {
141
- max-width: 1400px;
142
- margin: 0 auto;
143
- }
144
- .header {
145
- text-align: center;
146
- margin-bottom: 30px;
147
- }
148
- h1 {
149
- color: white;
150
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
151
- margin-bottom: 10px;
152
- }
153
- .subtitle {
154
- color: rgba(255,255,255,0.9);
155
- font-size: 0.9em;
156
- }
157
- .stats {
158
- display: flex;
159
- gap: 20px;
160
- margin-bottom: 30px;
161
- flex-wrap: wrap;
162
- }
163
- .stat-card {
164
- background: white;
165
- padding: 20px;
166
- border-radius: 10px;
167
- flex: 1;
168
- min-width: 200px;
169
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
170
- animation: slideIn 0.5s ease;
171
- }
172
- .stat-card h3 {
173
- color: #667eea;
174
- margin-bottom: 10px;
175
- font-size: 0.9em;
176
- text-transform: uppercase;
177
- letter-spacing: 1px;
178
- }
179
- .stat-card .number {
180
- font-size: 2em;
181
- font-weight: bold;
182
- color: #333;
183
- }
184
- .controls {
185
- margin-bottom: 20px;
186
- display: flex;
187
- gap: 10px;
188
- flex-wrap: wrap;
189
- }
190
- .refresh-btn, .reload-sheet-btn {
191
- background: white;
192
- color: #667eea;
193
- padding: 10px 20px;
194
- border: 2px solid #667eea;
195
- border-radius: 5px;
196
- cursor: pointer;
197
- font-weight: bold;
198
- transition: all 0.3s;
199
- }
200
- .refresh-btn:hover, .reload-sheet-btn:hover {
201
- background: #667eea;
202
- color: white;
203
- transform: translateY(-2px);
204
- }
205
- .bot-grid {
206
- display: grid;
207
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
208
- gap: 20px;
209
- }
210
- .bot-card {
211
- background: white;
212
- border-radius: 10px;
213
- padding: 20px;
214
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
215
- transition: all 0.3s;
216
- animation: fadeIn 0.5s ease;
217
- }
218
- .bot-card:hover {
219
- transform: translateY(-5px);
220
- box-shadow: 0 8px 15px rgba(0,0,0,0.2);
221
- }
222
- .bot-name {
223
- font-size: 1.1em;
224
- font-weight: bold;
225
- color: #333;
226
- margin-bottom: 10px;
227
- white-space: nowrap;
228
- overflow: hidden;
229
- text-overflow: ellipsis;
230
- }
231
- .bot-info {
232
- color: #666;
233
- margin: 5px 0;
234
- font-size: 0.9em;
235
- }
236
- .status {
237
- display: inline-block;
238
- padding: 5px 12px;
239
- border-radius: 20px;
240
- font-size: 0.85em;
241
- font-weight: bold;
242
- margin: 10px 0;
243
- text-transform: uppercase;
244
- letter-spacing: 0.5px;
245
- }
246
- .status.connected {
247
- background: linear-gradient(135deg, #10b981, #059669);
248
- color: white;
249
- }
250
- .status.disconnected {
251
- background: linear-gradient(135deg, #ef4444, #dc2626);
252
- color: white;
253
- }
254
- .status.connecting {
255
- background: linear-gradient(135deg, #f59e0b, #d97706);
256
- color: white;
257
- animation: pulse 1.5s infinite;
258
- }
259
- @keyframes pulse {
260
- 0%, 100% { opacity: 1; }
261
- 50% { opacity: 0.7; }
262
- }
263
- @keyframes slideIn {
264
- from { transform: translateX(-20px); opacity: 0; }
265
- to { transform: translateX(0); opacity: 1; }
266
- }
267
- @keyframes fadeIn {
268
- from { opacity: 0; }
269
- to { opacity: 1; }
270
- }
271
- .btn {
272
- padding: 8px 16px;
273
- border: none;
274
- border-radius: 5px;
275
- cursor: pointer;
276
- font-weight: bold;
277
- transition: all 0.3s;
278
- margin-top: 10px;
279
- font-size: 0.9em;
280
- }
281
- .btn-rejoin {
282
- background: linear-gradient(135deg, #667eea, #764ba2);
283
- color: white;
284
- }
285
- .btn-rejoin:hover:not(:disabled) {
286
- transform: scale(1.05);
287
- box-shadow: 0 4px 10px rgba(102, 126, 234, 0.4);
288
- }
289
- .btn:disabled {
290
- opacity: 0.5;
291
- cursor: not-allowed;
292
- }
293
- .timer {
294
- font-size: 0.85em;
295
- color: #999;
296
- margin-top: 5px;
297
- }
298
- .loading {
299
- text-align: center;
300
- color: white;
301
- font-size: 1.2em;
302
- margin-top: 50px;
303
- }
304
- .error-msg {
305
- background: #fee;
306
- color: #c00;
307
- padding: 10px;
308
- border-radius: 5px;
309
- margin: 10px 0;
310
- }
311
- </style>
312
- </head>
313
- <body>
314
- <div class="container">
315
- <div class="header">
316
- <h1>🤖 Mineflayer Bot Manager</h1>
317
- <div class="subtitle">Manage your Minecraft bots from Google Sheets</div>
318
- </div>
319
-
320
- <div class="stats">
321
- <div class="stat-card">
322
- <h3>Total Bots</h3>
323
- <div class="number" id="total-bots">0</div>
324
- </div>
325
- <div class="stat-card">
326
- <h3>Connected</h3>
327
- <div class="number" id="connected-bots">0</div>
328
- </div>
329
- <div class="stat-card">
330
- <h3>Disconnected</h3>
331
- <div class="number" id="disconnected-bots">0</div>
332
- </div>
333
- <div class="stat-card">
334
- <h3>Connecting</h3>
335
- <div class="number" id="connecting-bots">0</div>
336
- </div>
337
- </div>
338
-
339
- <div class="controls">
340
- <button class="refresh-btn" onclick="refreshBots()">🔄 Refresh Status</button>
341
- <button class="reload-sheet-btn" onclick="reloadSheet()">📊 Reload from Sheet</button>
342
- </div>
343
-
344
- <div id="error-container"></div>
345
-
346
- <div class="bot-grid" id="bot-grid">
347
- <div class="loading">Loading bots...</div>
348
- </div>
349
- </div>
350
-
351
- <script>
352
- let botsData = {};
353
-
354
- async function fetchBots() {
355
- try {
356
- const response = await fetch('/api/bots');
357
- if (!response.ok) throw new Error('Failed to fetch');
358
- botsData = await response.json();
359
- updateUI();
360
- clearError();
361
- } catch (error) {
362
- console.error('Error fetching bots:', error);
363
- showError('Failed to fetch bot status');
364
- }
365
- }
366
-
367
- async function rejoinBot(botName) {
368
- if (!confirm(`Rejoin bot ${botName}?`)) return;
369
-
370
- try {
371
- const response = await fetch('/api/rejoin', {
372
- method: 'POST',
373
- headers: {'Content-Type': 'application/json'},
374
- body: JSON.stringify({bot_name: botName})
375
- });
376
- const result = await response.json();
377
- if (result.error) {
378
- alert(result.error);
379
- } else {
380
- await fetchBots();
381
- }
382
- } catch (error) {
383
- console.error('Error rejoining bot:', error);
384
- alert('Failed to rejoin bot');
385
- }
386
- }
387
-
388
- async function refreshBots() {
389
- await fetchBots();
390
- }
391
-
392
- async function reloadSheet() {
393
- try {
394
- const response = await fetch('/api/refresh', {method: 'POST'});
395
- if (!response.ok) throw new Error('Failed to reload');
396
- await fetchBots();
397
- clearError();
398
- } catch (error) {
399
- console.error('Error reloading sheet:', error);
400
- showError('Failed to reload from sheet');
401
- }
402
- }
403
-
404
- function showError(message) {
405
- const container = document.getElementById('error-container');
406
- container.innerHTML = `<div class="error-msg">${message}</div>`;
407
- }
408
-
409
- function clearError() {
410
- document.getElementById('error-container').innerHTML = '';
411
- }
412
-
413
- function formatTime(seconds) {
414
- const minutes = Math.floor(seconds / 60);
415
- const secs = seconds % 60;
416
- return `${minutes}m ${secs}s`;
417
- }
418
-
419
- function updateUI() {
420
- const grid = document.getElementById('bot-grid');
421
- grid.innerHTML = '';
422
-
423
- let total = 0;
424
- let connected = 0;
425
- let disconnected = 0;
426
- let connecting = 0;
427
-
428
- for (const [botName, botInfo] of Object.entries(botsData)) {
429
- total++;
430
- if (botInfo.status === 'connected') connected++;
431
- else if (botInfo.status === 'disconnected') disconnected++;
432
- else if (botInfo.status === 'connecting') connecting++;
433
-
434
- const card = document.createElement('div');
435
- card.className = 'bot-card';
436
-
437
- let statusClass = botInfo.status;
438
- let canRejoin = botInfo.can_rejoin;
439
- let timeRemaining = '';
440
-
441
- if (botInfo.time_until_rejoin && botInfo.time_until_rejoin > 0) {
442
- timeRemaining = `⏱️ ${formatTime(botInfo.time_until_rejoin)}`;
443
- }
444
-
445
- // Escape bot name for HTML
446
- const safeBotName = botName.replace(/'/g, "\\'").replace(/"/g, "&quot;");
447
-
448
- card.innerHTML = `
449
- <div class="bot-name" title="${safeBotName}">🎮 ${safeBotName}</div>
450
- <div class="bot-info">📦 Version: ${botInfo.version}</div>
451
- <div class="status ${statusClass}">${botInfo.status}</div>
452
- ${timeRemaining ? `<div class="timer">${timeRemaining}</div>` : ''}
453
- ${botInfo.status === 'disconnected' ?
454
- `<button class="btn btn-rejoin"
455
- onclick="rejoinBot('${safeBotName}')"
456
- ${!canRejoin ? 'disabled' : ''}>
457
- ${canRejoin ? '🔄 Rejoin' : '⏳ ' + (timeRemaining || 'Cooldown')}
458
- </button>` : ''}
459
- `;
460
-
461
- grid.appendChild(card);
462
- }
463
-
464
- if (total === 0) {
465
- grid.innerHTML = '<div class="loading">No bots configured. Add bots to your Google Sheet.</div>';
466
- }
467
-
468
- document.getElementById('total-bots').textContent = total;
469
- document.getElementById('connected-bots').textContent = connected;
470
- document.getElementById('disconnected-bots').textContent = disconnected;
471
- document.getElementById('connecting-bots').textContent = connecting;
472
- }
473
-
474
- // Initial fetch and periodic updates
475
- fetchBots();
476
- setInterval(fetchBots, 3000); // Update every 3 seconds
477
- </script>
478
- </body>
479
- </html>
480
  """
481
 
482
  def write_bot_script():
483
- """Write the bot script to /tmp directory"""
 
484
  try:
485
- os.makedirs(WORK_DIR, exist_ok=True)
486
- with open(BOT_SCRIPT_PATH, 'w') as f:
487
- f.write(BOT_SCRIPT)
488
- print(f"Bot script written to {BOT_SCRIPT_PATH}")
489
- return True
490
  except Exception as e:
491
  print(f"Error writing bot script: {e}")
492
- return False
493
 
494
- def fetch_sheet_data():
495
- """Fetch bot configuration from Google Sheets"""
496
  try:
497
- response = requests.get(SHEET_URL, timeout=10)
498
- if response.status_code == 200:
499
- lines = response.text.strip().split('\n')
500
- if len(lines) > 1:
501
- data = []
502
- for line in lines[1:]: # Skip header
503
- # Handle CSV parsing properly
504
- parts = line.split(',')
505
- if len(parts) >= 3:
506
- bot_name = parts[0].strip().strip('"')
507
- ip = parts[1].strip().strip('"')
508
- port = parts[2].strip().strip('"')
509
- version = parts[3].strip().strip('"') if len(parts) > 3 else "1.20.1"
510
-
511
- # Skip empty rows
512
- if bot_name and ip and port and port.isdigit():
513
- data.append({
514
- 'bot_name': bot_name,
515
- 'ip': ip,
516
- 'port': port,
517
- 'version': version if version else "1.20.1"
518
- })
519
- print(f"Fetched {len(data)} bots from sheet")
520
- return data
 
521
  except Exception as e:
522
- print(f"Error fetching sheet: {e}")
523
- return []
524
 
525
- def start_bot(bot_data):
526
- """Start a Minecraft bot"""
527
- bot_name = bot_data['bot_name']
528
- server_key = f"{bot_data['ip']}:{bot_data['port']}"
529
 
530
  # Check if server already has a bot
531
- if server_key in server_bots and server_bots[server_key] != bot_name:
532
  return False, "Server already has a bot"
533
 
534
- # Kill existing process if any
535
- if bot_name in bot_processes:
536
  try:
537
- process = bot_processes[bot_name]
538
- process.terminate()
539
- process.wait(timeout=5)
540
  except:
541
  pass
542
 
543
- # Start new bot process
544
  try:
545
- # Change working directory to /app where node_modules are installed
546
- process = subprocess.Popen(
547
- ['node', BOT_SCRIPT_PATH, bot_name, bot_data['ip'], bot_data['port'], bot_data['version']],
548
  stdout=subprocess.PIPE,
549
  stderr=subprocess.STDOUT,
550
  text=True,
551
- bufsize=1,
552
- cwd='/app' # Use /app as working directory
553
  )
554
 
555
- bot_processes[bot_name] = process
556
- server_bots[server_key] = bot_name
557
 
558
- bots[bot_name] = {
559
  'status': 'connecting',
560
- 'ip': bot_data['ip'],
561
- 'port': bot_data['port'],
562
- 'version': bot_data['version'],
563
- 'server_key': server_key
564
  }
565
 
566
- # Start monitoring thread
567
- threading.Thread(target=monitor_bot, args=(bot_name,), daemon=True).start()
568
- return True, "Bot started"
 
569
  except Exception as e:
570
- print(f"Error starting bot {bot_name}: {e}")
571
  return False, str(e)
572
 
573
- def monitor_bot(bot_name):
574
- """Monitor bot process output"""
575
- if bot_name not in bot_processes:
576
  return
577
 
578
- process = bot_processes[bot_name]
579
 
580
- while True:
581
- if process.poll() is not None:
582
- # Process ended
583
- if bot_name in bots:
584
- bots[bot_name]['status'] = 'disconnected'
585
- server_key = bots[bot_name].get('server_key')
586
- if server_key and server_key in server_bots and server_bots[server_key] == bot_name:
587
- del server_bots[server_key]
588
- break
589
-
590
- try:
591
- line = process.stdout.readline()
592
- if line:
593
- print(f"[{bot_name}] {line.strip()}")
594
- if 'CONNECTED' in line:
595
- if bot_name in bots:
596
- bots[bot_name]['status'] = 'connected'
597
- elif any(x in line for x in ['DISCONNECTED', 'KICKED', 'ERROR', 'DIED']):
598
- if bot_name in bots:
599
- bots[bot_name]['status'] = 'disconnected'
600
- except:
601
- pass
602
-
603
- time.sleep(0.1)
604
-
605
- def update_bots_from_sheet():
606
- """Update bots from Google Sheet"""
607
- print("Updating bots from sheet...")
608
- sheet_data = fetch_sheet_data()
609
-
610
- # Remove bots that are no longer in sheet
611
- current_bot_names = {bot['bot_name'] for bot in sheet_data}
612
- bots_to_remove = []
613
 
614
- for bot_name in list(bots.keys()):
615
- if bot_name not in current_bot_names:
616
- bots_to_remove.append(bot_name)
 
 
 
 
 
 
 
 
617
 
618
- for bot_name in bots_to_remove:
619
- if bot_name in bot_processes:
620
- try:
621
- bot_processes[bot_name].terminate()
622
- except:
623
- pass
624
- if bot_name in bots:
625
- server_key = bots[bot_name].get('server_key')
626
- if server_key in server_bots:
627
- del server_bots[server_key]
628
- del bots[bot_name]
629
- print(f"Removed bot: {bot_name}")
 
630
 
631
  # Add new bots
632
- for bot_data in sheet_data:
633
- bot_name = bot_data['bot_name']
634
-
635
- if bot_name not in bots:
636
- success, message = start_bot(bot_data)
637
- print(f"Starting {bot_name}: {message}")
638
 
639
  @app.route('/')
640
  def index():
641
- return render_template_string(HTML_TEMPLATE)
642
 
643
  @app.route('/api/bots')
644
- def get_bots():
645
- """Get all bot statuses"""
646
  result = {}
647
- current_time = datetime.now()
648
 
649
- for bot_name, bot_info in bots.items():
650
  can_rejoin = True
651
- time_until_rejoin = 0
652
 
653
- if bot_name in last_rejoin_times:
654
- time_passed = current_time - last_rejoin_times[bot_name]
655
- if time_passed < timedelta(hours=1):
656
  can_rejoin = False
657
- time_until_rejoin = int((timedelta(hours=1) - time_passed).total_seconds())
658
 
659
- result[bot_name] = {
660
- 'status': bot_info['status'],
661
- 'version': bot_info['version'],
662
  'can_rejoin': can_rejoin,
663
- 'time_until_rejoin': time_until_rejoin
664
  }
665
 
666
  return jsonify(result)
667
 
668
  @app.route('/api/rejoin', methods=['POST'])
669
- def rejoin_bot():
670
- """Rejoin a disconnected bot"""
671
- data = request.json
672
- bot_name = data.get('bot_name')
673
 
674
- if bot_name not in bots:
675
  return jsonify({'error': 'Bot not found'}), 404
676
 
677
- current_time = datetime.now()
678
 
679
  # Check cooldown
680
- if bot_name in last_rejoin_times:
681
- time_passed = current_time - last_rejoin_times[bot_name]
682
- if time_passed < timedelta(hours=1):
683
- remaining = timedelta(hours=1) - time_passed
684
- return jsonify({'error': f'Please wait {int(remaining.total_seconds() / 60)} minutes'}), 429
685
 
686
- if bots[bot_name]['status'] == 'connected':
687
- return jsonify({'error': 'Bot is already connected'}), 400
688
 
689
- bot_data = {
690
- 'bot_name': bot_name,
691
- 'ip': bots[bot_name]['ip'],
692
- 'port': bots[bot_name]['port'],
693
- 'version': bots[bot_name]['version']
694
  }
695
 
696
- success, message = start_bot(bot_data)
697
 
698
  if success:
699
- last_rejoin_times[bot_name] = current_time
700
- return jsonify({'success': True, 'message': message})
701
  else:
702
- return jsonify({'error': message}), 500
703
 
704
- @app.route('/api/refresh', methods=['POST'])
705
- def refresh():
706
- """Refresh bots from Google Sheet"""
707
- update_bots_from_sheet()
708
  return jsonify({'success': True})
709
 
710
- def signal_handler(sig, frame):
711
- """Handle shutdown gracefully"""
712
- print("\nShutting down gracefully...")
713
- for bot_name, process in bot_processes.items():
714
  try:
715
- process.terminate()
716
  except:
717
  pass
718
  sys.exit(0)
719
 
720
  if __name__ == '__main__':
721
- signal.signal(signal.SIGINT, signal_handler)
722
- signal.signal(signal.SIGTERM, signal_handler)
723
 
724
- # Write bot script to /tmp
725
- if not write_bot_script():
726
- print("Failed to write bot script, exiting...")
 
727
  sys.exit(1)
728
 
729
- # Initial load from sheet
730
- print("Loading bots from Google Sheet...")
731
- update_bots_from_sheet()
 
732
 
733
- # Start periodic sheet updates
734
- def periodic_update():
735
  while True:
736
- time.sleep(60) # Check every minute
737
  try:
738
- update_bots_from_sheet()
739
  except Exception as e:
740
- print(f"Error in periodic update: {e}")
741
 
742
- threading.Thread(target=periodic_update, daemon=True).start()
743
 
744
- # Start Flask app
745
- print("Starting web server on port 7860...")
746
  app.run(host='0.0.0.0', port=7860, debug=False)
 
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
19
  bots = {}
20
  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
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  """
224
 
225
  def write_bot_script():
226
+ """Write bot script to temp directory"""
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:
241
+ return []
242
+
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,
290
  stderr=subprocess.STDOUT,
291
  text=True,
292
+ bufsize=1
 
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
 
318
+ proc = bot_processes[name]
319
 
320
+ try:
321
+ while proc.poll() is None:
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
 
341
+ # Process ended
342
+ if name in bots:
343
+ bots[name]['status'] = 'offline'
344
+ server = bots[name].get('server')
345
+ if server in server_locks and server_locks[server] == name:
346
+ del server_locks[server]
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
354
+ for name in list(bots.keys()):
355
+ if name not in names:
356
+ if name in bot_processes:
357
+ try:
358
+ bot_processes[name].terminate()
359
+ except:
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():
374
+ return HTML
375
 
376
  @app.route('/api/bots')
377
+ def api_bots():
378
+ """Get bot status"""
379
  result = {}
380
+ now = datetime.now()
381
 
382
+ for name, bot in bots.items():
383
  can_rejoin = True
384
+ cooldown = 0
385
 
386
+ if name in last_rejoin:
387
+ elapsed = now - last_rejoin[name]
388
+ if elapsed < timedelta(hours=1):
389
  can_rejoin = False
390
+ cooldown = int((timedelta(hours=1) - elapsed).total_seconds())
391
 
392
+ result[name] = {
393
+ 'status': bot['status'],
394
+ 'version': bot['version'],
395
  'can_rejoin': can_rejoin,
396
+ 'cooldown': cooldown
397
  }
398
 
399
  return jsonify(result)
400
 
401
  @app.route('/api/rejoin', methods=['POST'])
402
+ def api_rejoin():
403
+ """Rejoin a bot"""
404
+ name = request.json.get('name')
 
405
 
406
+ if name not in bots:
407
  return jsonify({'error': 'Bot not found'}), 404
408
 
409
+ now = datetime.now()
410
 
411
  # Check cooldown
412
+ if name in last_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,
423
+ 'ip': bots[name]['ip'],
424
+ 'port': bots[name]['port'],
425
+ 'version': bots[name]['version']
426
  }
427
 
428
+ success, msg = start_bot(config)
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:
449
  pass
450
  sys.exit(0)
451
 
452
  if __name__ == '__main__':
453
+ signal.signal(signal.SIGINT, cleanup)
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)