const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const mineflayer = require('mineflayer'); const mc = require('minecraft-protocol'); const path = require('path'); const fs = require('fs'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*', methods: ['GET', 'POST'] } }); const PORT = process.env.PORT || 7860; // Serve static files from the 'public' directory app.use(express.static(path.join(__dirname, 'public'))); // Fallback to index.html for any request app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Bot store // structure: username -> { instance, config, status, reconnectTimer, shouldReconnect, logs: [] } const bots = {}; // Function to send and store logs function sendLog(username, type, message) { const logEntry = { username, type, // 'info', 'chat', 'error', 'success', 'warn' message, timestamp: new Date().toLocaleTimeString() }; if (bots[username]) { if (!bots[username].logs) bots[username].logs = []; bots[username].logs.push(logEntry); if (bots[username].logs.length > 200) { bots[username].logs.shift(); } } io.emit('bot-log', logEntry); console.log(`[${username}] [${type.toUpperCase()}] ${message}`); } // Function to update and broadcast bot status function updateBotStatus(username, status, config = null) { if (bots[username]) { bots[username].status = status; if (config) { bots[username].config = config; } } io.emit('bot-status-change', { username, status, config: bots[username] ? bots[username].config : config }); } // Create and initialize a Mineflayer Bot function createBot(config) { const { username, host, port, startupCommand, version, auth } = config; const botPort = parseInt(port) || 25565; // Clean up any existing bot instance with the same name if (bots[username] && bots[username].instance) { try { bots[username].instance.end(); } catch (e) {} } // Clear existing reconnect timer if present if (bots[username] && bots[username].reconnectTimer) { clearTimeout(bots[username].reconnectTimer); bots[username].reconnectTimer = null; } // Initialize bot entry if not exists (preserves logs) if (!bots[username]) { bots[username] = { instance: null, config, status: 'connecting', reconnectTimer: null, shouldReconnect: true, logs: [] }; } else { bots[username].config = config; bots[username].status = 'connecting'; bots[username].shouldReconnect = true; } sendLog(username, 'info', `Verbindungsaufbau zu ${host}:${botPort} als ${username}...`); updateBotStatus(username, 'connecting'); const botOptions = { host: host, port: botPort, username: username, version: version || false, auth: auth || 'offline', connectTimeout: 30000 }; // Microsoft account authentication support let spawnTimeout; if (auth === 'microsoft') { const authCacheDir = path.join(__dirname, '.auth-cache'); try { if (!fs.existsSync(authCacheDir)) { fs.mkdirSync(authCacheDir, { recursive: true }); } } catch (e) {} botOptions.profilesFolder = authCacheDir; botOptions.onMsaCode = (data) => { // Clear spawn timeout during auth – MS flow can take up to 15min clearTimeout(spawnTimeout); sendLog(username, 'warn', `Microsoft-Authentifizierung erforderlich! Besuche ${data.verification_uri || 'https://microsoft.com/link'} und gib den Code ${data.user_code} ein.`); io.emit('microsoft-device-code', { username, url: data.verification_uri || 'https://microsoft.com/link', code: data.user_code, message: data.message || `Öffne ${data.verification_uri} und gib den Code ${data.user_code} ein.` }); }; } let bot; try { bot = mineflayer.createBot(botOptions); } catch (err) { sendLog(username, 'error', `Fehler beim Erstellen des Bots: ${err.message}`); scheduleReconnect(username, config); return; } bots[username].instance = bot; // Spawn timeout: disconnect and retry if bot doesn't spawn within 60s spawnTimeout = setTimeout(() => { if (bots[username] && bots[username].status === 'connecting') { sendLog(username, 'error', 'Timeout: Bot wurde nicht gespawnt (60s). Starte Wiederverbindung...'); try { bot.end(); } catch (e) {} } }, 60000); // Setup Mineflayer event handlers bot.on('spawn', () => { clearTimeout(spawnTimeout); if (!bots[username]) return; bots[username].status = 'online'; sendLog(username, 'success', `Bot erfolgreich eingeloggt und gespawnt!`); updateBotStatus(username, 'online'); // Run startup command if provided if (startupCommand && startupCommand.trim()) { sendLog(username, 'info', `Führe Startup-Befehl aus: "${startupCommand}"`); setTimeout(() => { if (bots[username] && bots[username].instance && bots[username].status === 'online') { try { bots[username].instance.chat(startupCommand); } catch (e) { sendLog(username, 'error', `Startup-Befehl fehlgeschlagen: ${e.message}`); } } }, 2500); // Wait 2.5s to ensure the bot is fully ready to chat } }); // Accept resource packs to avoid being kicked bot.on('resourcePack', () => { sendLog(username, 'info', 'Server fordert Resource Pack an – wird automatisch akzeptiert.'); try { bot.acceptResourcePack(); } catch (e) {} }); bot.on('message', (jsonMsg) => { const cleanMsg = jsonMsg.toString(); if (cleanMsg.trim()) { sendLog(username, 'chat', cleanMsg); } }); bot.on('kicked', (reason) => { let cleanReason = reason; try { const parsed = JSON.parse(reason); if (parsed.text) cleanReason = parsed.text; else if (parsed.extra) cleanReason = parsed.extra.map(x => x.text || '').join(''); } catch (e) {} sendLog(username, 'warn', `Vom Server gekickt. Grund: ${cleanReason || reason}`); }); bot.on('error', (err) => { sendLog(username, 'error', `Verbindungsfehler: ${err.message}`); }); bot.on('end', () => { if (!bots[username]) return; sendLog(username, 'info', 'Verbindung getrennt.'); updateBotStatus(username, 'offline'); if (bots[username].shouldReconnect) { scheduleReconnect(username, config); } else { delete bots[username]; } }); } // Schedule a reconnection attempt function scheduleReconnect(username, config) { if (!bots[username] || !bots[username].shouldReconnect) return; if (bots[username].reconnectTimer) { clearTimeout(bots[username].reconnectTimer); } updateBotStatus(username, 'reconnecting'); sendLog(username, 'warn', 'Verbindung verloren. Automatischer Wiederverbindungsversuch in 5 Sekunden...'); bots[username].reconnectTimer = setTimeout(() => { if (bots[username] && bots[username].shouldReconnect) { createBot(config); } }, 5000); } // Stop and terminate a bot function stopBot(username) { const botData = bots[username]; if (!botData) return; sendLog(username, 'info', 'Bot wird manuell gestoppt...'); botData.shouldReconnect = false; if (botData.reconnectTimer) { clearTimeout(botData.reconnectTimer); botData.reconnectTimer = null; } if (botData.instance) { try { botData.instance.quit(); } catch (e) { try { botData.instance.end(); } catch (e2) {} } } updateBotStatus(username, 'offline'); delete bots[username]; sendLog(username, 'info', 'Bot wurde erfolgreich gestoppt.'); } // Socket.io connection handling io.on('connection', (socket) => { console.log(`Socket verbunden: ${socket.id}`); // Send initial state of all bots const botStates = Object.keys(bots).map(username => ({ username, status: bots[username].status, config: bots[username].config, logs: bots[username].logs || [] })); socket.emit('init-state', botStates); // Ping server to check version and status socket.on('ping-server', ({ host, port }) => { if (!host || !host.trim()) { socket.emit('ping-result', { error: 'Server-IP erforderlich!' }); return; } const cleanHost = host.trim(); const cleanPort = parseInt(port) || 25565; mc.ping({ host: cleanHost, port: cleanPort }, (err, result) => { if (err) { socket.emit('ping-result', { error: `Server nicht erreichbar: ${err.message}` }); } else { socket.emit('ping-result', { host: cleanHost, port: cleanPort, version: result.version.name, protocol: result.version.protocol, motd: result.description?.text || JSON.stringify(result.description), players: result.players?.online || 0, maxPlayers: result.players?.max || 0, latency: result.latency }); } }); }); // Start bot request socket.on('start-bot', (config) => { const { username, host } = config; if (!username || !username.trim() || !host || !host.trim()) { socket.emit('error-msg', 'Fehler: Server-IP und Bot-Name sind erforderlich!'); return; } const cleanUsername = username.trim(); createBot({ username: cleanUsername, host: host.trim(), port: config.port || 25565, startupCommand: config.startupCommand || '', version: config.version || '', auth: config.auth || 'offline' }); }); // Stop bot request socket.on('stop-bot', (username) => { if (username && bots[username]) { stopBot(username); } }); // Direct chat input socket.on('send-chat', ({ username, message }) => { const botData = bots[username]; if (botData && botData.instance && botData.status === 'online') { try { botData.instance.chat(message); sendLog(username, 'chat', `[Du] ${message}`); } catch (e) { sendLog(username, 'error', `Fehler beim Senden der Nachricht: ${e.message}`); } } else { socket.emit('error-msg', `Fehler: Bot "${username}" ist nicht online oder existiert nicht.`); } }); socket.on('disconnect', () => { console.log(`Socket getrennt: ${socket.id}`); }); }); // Process safety nets process.on('uncaughtException', (err) => { console.error('System Uncaught Exception:', err); }); process.on('unhandledRejection', (reason, promise) => { console.error('System Unhandled Rejection:', reason); }); // Start listening server.listen(PORT, '0.0.0.0', () => { console.log(`=== Mineflayer Bot Manager läuft auf Port ${PORT} ===`); console.log(`Hugging Face Spaces bereit unter: http://localhost:${PORT}`); });