Spaces:
Sleeping
Sleeping
| 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}`); | |
| }); | |