Hugosmpbots / server.js
TheTapTap800
Fix spawn timeout race with Microsoft auth flow
a4313e4
Raw
History Blame Contribute Delete
10.9 kB
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}`);
});