const https = require("https"); const fs = require("fs"); const path = require("path"); const mineflayer = require("mineflayer"); require("dotenv").config(); const REJOIN_DELAY_MS = 60_000; const SECURITY_ARM_DELAY_MS = 5_000; const SECURITY_RADIUS = 20; const EMERGENCY_HOTBAR_SLOT = 0; const EMERGENCY_MISSING_RETRIES = 8; const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || ""; const ALLOWED_PLAYERS_FILE = process.env.ALLOWED_PLAYERS_FILE || "allowed_players.txt"; const BOT_CONFIG = { host: "java.donutsmp.net", username: "Tomek", auth: "microsoft", version: "1.21.1", }; let bot = null; let hasJoined = false; let spawnerPosition = null; let emergencyTriggered = false; let securityMonitorInterval = null; let securityArmed = false; let securityArmTimer = null; let reconnectTimer = null; const allowedPlayers = loadAllowedPlayers(); function pushLog(message) { console.log(message); } function loadAllowedPlayers() { const filePath = path.isAbsolute(ALLOWED_PLAYERS_FILE) ? ALLOWED_PLAYERS_FILE : path.join(__dirname, ALLOWED_PLAYERS_FILE); try { const data = fs.readFileSync(filePath, "utf8"); return new Set( data .split(/\r?\n/) .map((line) => line.trim().toLowerCase()) .filter((line) => line && !line.startsWith("#")), ); } catch { pushLog(`allowed players file not found: ${filePath}`); return new Set(); } } function stringifyReason(reason) { if (typeof reason === "string") return reason; try { return JSON.stringify(reason); } catch { return String(reason); } } function scheduleRejoinIfAlreadyOnline(reason) { const reasonText = stringifyReason(reason); pushLog(`kicked: ${reasonText}`); if (!reasonText.includes("You are already online")) return; if (reconnectTimer) { pushLog("rejoin already scheduled"); return; } pushLog("already online kick detected, rejoining in 60s"); reconnectTimer = setTimeout(() => { reconnectTimer = null; pushLog("rejoining now..."); createBotInstance(); }, REJOIN_DELAY_MS); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isSpawnerBlock(block) { if (!block) return false; return ["spawner", "mob_spawner", "trial_spawner"].includes(block.name); } function findSpawnerBlock(maxDistance = 6) { const matching = ["spawner", "mob_spawner", "trial_spawner"] .filter((name) => bot.registry.blocksByName[name] !== undefined) .map((name) => bot.registry.blocksByName[name].id); if (matching.length === 0) return null; return bot.findBlock({ matching, maxDistance, }); } function getUnauthorizedNearbyPlayers() { if (!bot || !bot.entity || !bot.entity.position) return []; const intruders = []; for (const [username, player] of Object.entries(bot.players)) { if (!username || username === bot.username) continue; if (allowedPlayers.has(username.toLowerCase())) continue; if (!player || !player.entity || !player.entity.position) continue; const distance = player.entity.position.distanceTo(bot.entity.position); if (distance <= SECURITY_RADIUS) { intruders.push(`${username} (${distance.toFixed(1)}m)`); } } return intruders; } function waitForWindowOpen(timeoutMs = 5000) { return new Promise((resolve) => { const timeout = setTimeout(() => { bot.removeListener("windowOpen", onOpen); resolve(false); }, timeoutMs); const onOpen = () => { clearTimeout(timeout); resolve(true); }; bot.once("windowOpen", onOpen); }); } function closeCurrentWindow() { if (!bot.currentWindow) return; if (typeof bot.closeWindow === "function") { bot.closeWindow(bot.currentWindow); return; } if (typeof bot.currentWindow.close === "function") { bot.currentWindow.close(); } } function isSpawnerItem(item) { if (!item) return false; return ["spawner", "mob_spawner", "trial_spawner"].includes( String(item.name || "").toLowerCase(), ); } function countSpawnerItemsInInventory() { if (!bot || !bot.inventory) return 0; return bot.inventory .items() .filter((item) => isSpawnerItem(item)) .reduce((sum, item) => sum + (item.count || 0), 0); } function getInventoryStart(window = bot.currentWindow) { if (!window) return 9; if (Number.isFinite(window.inventoryStart)) return window.inventoryStart; if (Array.isArray(window.slots) && window.slots.length >= 36) { return Math.max(0, window.slots.length - 36); } return 9; } function getInventorySpawnerSlotsInCurrentWindow() { if (!bot.currentWindow || !Array.isArray(bot.currentWindow.slots)) return []; const slots = bot.currentWindow.slots; const inventoryStart = getInventoryStart(bot.currentWindow); const result = []; for (let i = inventoryStart; i < slots.length; i += 1) { const item = slots[i]; if (!isSpawnerItem(item)) continue; result.push(i); } return result; } function findNearbyEnderChest(maxDistance = 6) { const blockDef = bot?.registry?.blocksByName?.ender_chest; if (!blockDef) return null; return bot.findBlock({ matching: blockDef.id, maxDistance, }); } async function moveAllInventorySpawnersToCurrentWindow() { if (!bot.currentWindow) return 0; let moved = 0; let safety = 0; while (safety < 120) { safety += 1; const before = countSpawnerItemsInInventory(); if (before === 0) break; const spawnerSlots = getInventorySpawnerSlotsInCurrentWindow(); if (spawnerSlots.length === 0) break; let clicked = false; for (const slot of spawnerSlots) { try { await bot.clickWindow(slot, 0, 1); clicked = true; await sleep(120); } catch { // ignore click failures and continue trying other slots } } const after = countSpawnerItemsInInventory(); if (after < before) { moved += before - after; continue; } if (!clicked) break; break; } pushLog(`moved spawners from inventory: ${moved}`); return moved; } async function stashSpawnerDropsInEnderChest() { const before = countSpawnerItemsInInventory(); if (before === 0) { pushLog("no spawner items in inventory to stash"); return { attempted: false, moved: 0, remaining: 0 }; } const enderChest = findNearbyEnderChest(6); if (!enderChest) { pushLog("no nearby ender chest found for stashing spawners"); return { attempted: true, moved: 0, remaining: before }; } try { await bot.lookAt(enderChest.position.offset(0.5, 0.5, 0.5), true); const openPromise = waitForWindowOpen(5000); closeCurrentWindow(); await bot.activateBlock(enderChest); const opened = await openPromise; if (!opened || !bot.currentWindow) { pushLog("ender chest did not open"); return { attempted: true, moved: 0, remaining: countSpawnerItemsInInventory(), }; } await sleep(150); const moved = await moveAllInventorySpawnersToCurrentWindow(); closeCurrentWindow(); const remaining = countSpawnerItemsInInventory(); if (remaining === 0) { pushLog("all spawner items stashed in ender chest"); } else { pushLog(`spawner stash incomplete, remaining in inventory: ${remaining}`); } return { attempted: true, moved, remaining }; } catch (err) { pushLog(`failed stashing spawners in ender chest: ${err.message}`); closeCurrentWindow(); return { attempted: true, moved: 0, remaining: countSpawnerItemsInInventory(), }; } } async function breakSpawnerUntilGone() { await bot.setQuickBarSlot(EMERGENCY_HOTBAR_SLOT); await bot.setControlState("sneak", true); await sleep(250); let missingChecks = 0; try { while (true) { await bot.setControlState("sneak", true); let targetBlock = null; if (spawnerPosition) { targetBlock = bot.blockAt(spawnerPosition); } if (!isSpawnerBlock(targetBlock)) { targetBlock = findSpawnerBlock(); } if (!isSpawnerBlock(targetBlock)) { missingChecks += 1; if (missingChecks >= EMERGENCY_MISSING_RETRIES) { pushLog("spawner no longer present"); return true; } await sleep(200); continue; } missingChecks = 0; spawnerPosition = targetBlock.position.clone(); await bot.setControlState("sneak", true); await sleep(120); await bot.lookAt(targetBlock.position.offset(0.5, 0.5, 0.5), true); if (!bot.canDigBlock(targetBlock)) { pushLog("cannot dig spawner yet, retrying"); await sleep(300); continue; } try { await bot.dig(targetBlock, true); await sleep(150); } catch (err) { pushLog(`spawner dig failed: ${err.message}`); await sleep(300); } } } finally { bot.setControlState("sneak", false); } } async function sendDiscordWebhook(content) { if (!DISCORD_WEBHOOK_URL) { pushLog("DISCORD_WEBHOOK_URL not set; skipping webhook"); return; } try { const body = JSON.stringify({ content: `@everyone ${content}`, allowed_mentions: { parse: ["everyone"] }, }); const url = new URL(DISCORD_WEBHOOK_URL); await new Promise((resolve, reject) => { const req = https.request( { method: "POST", hostname: url.hostname, path: `${url.pathname}${url.search}`, port: url.port || 443, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), }, }, (res) => { res.on("data", () => {}); res.on("end", () => resolve()); }, ); req.on("error", reject); req.write(body); req.end(); }); } catch (err) { pushLog(`failed webhook send: ${err.message}`); } } async function triggerSecurityShutdown(reason) { if (emergencyTriggered) return; emergencyTriggered = true; pushLog(`security trigger: ${reason}`); const spawnerDestroyed = await breakSpawnerUntilGone(); const status = spawnerDestroyed ? "spawner removed" : "spawner not removed"; await sleep(600); const stashResult = await stashSpawnerDropsInEnderChest(); const stashStatus = stashResult.attempted ? stashResult.remaining === 0 ? `spawners stashed (moved=${stashResult.moved})` : `stash incomplete (moved=${stashResult.moved}, remaining=${stashResult.remaining})` : "no spawner items to stash"; await sendDiscordWebhook( `MCBOT ALERT: ${reason} | ${status} | ${stashStatus}`, ); try { bot.quit("security shutdown"); } catch { // ignore disconnect errors } } function startSecurityMonitor() { if (securityMonitorInterval) clearInterval(securityMonitorInterval); securityMonitorInterval = setInterval(() => { if (!hasJoined || !bot || emergencyTriggered || !securityArmed) return; const intruders = getUnauthorizedNearbyPlayers(); if (intruders.length > 0) { triggerSecurityShutdown( `unauthorized player nearby: ${intruders.join(", ")}`, ); } }, 1000); } function stopSecurityMonitor() { if (!securityMonitorInterval) return; clearInterval(securityMonitorInterval); securityMonitorInterval = null; } function armSecurityAfterJoinDelay() { if (securityArmTimer) { clearTimeout(securityArmTimer); securityArmTimer = null; } securityArmed = false; pushLog(`security will auto-enable in ${SECURITY_ARM_DELAY_MS / 1000}s`); securityArmTimer = setTimeout(() => { securityArmTimer = null; if (!bot || !hasJoined || emergencyTriggered) return; securityArmed = true; pushLog("security auto-enabled"); }, SECURITY_ARM_DELAY_MS); } function bindBotEvents() { bot.once("spawn", () => { hasJoined = true; if (bot.entity?.position) { const p = bot.entity.position; pushLog( `join position set: ${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)}`, ); } startSecurityMonitor(); pushLog("security monitor started"); armSecurityAfterJoinDelay(); }); bot.on("blockUpdate", (oldBlock, newBlock) => { if ( !hasJoined || emergencyTriggered || !securityArmed || !bot.entity || !bot.entity.position ) { return; } if (!oldBlock || !newBlock) return; if (oldBlock.name === "air") return; if (newBlock.name !== "air") return; const distance = oldBlock.position.distanceTo(bot.entity.position); if (distance <= SECURITY_RADIUS) { triggerSecurityShutdown( `block broken within ${SECURITY_RADIUS}m at ${oldBlock.position.x},${oldBlock.position.y},${oldBlock.position.z}`, ); } }); bot.on("kicked", (reason) => scheduleRejoinIfAlreadyOnline(reason)); bot.on("error", (err) => pushLog(`error: ${err.message}`)); bot.on("end", () => { stopSecurityMonitor(); if (securityArmTimer) { clearTimeout(securityArmTimer); securityArmTimer = null; } securityArmed = false; pushLog("bot disconnected"); }); } function createBotInstance() { hasJoined = false; spawnerPosition = null; emergencyTriggered = false; securityArmed = false; if (securityArmTimer) { clearTimeout(securityArmTimer); securityArmTimer = null; } bot = mineflayer.createBot(BOT_CONFIG); bindBotEvents(); } createBotInstance(); pushLog("mode=emergency-only (no loop/commands)"); pushLog( `security radius=${SECURITY_RADIUS}m, allowed players file=${ALLOWED_PLAYERS_FILE}`, );