From ff255bea38a017132bad99a2acb28f20e891543a Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Tue, 24 Mar 2026 23:43:07 +0100 Subject: [PATCH] feat: add emergency-only bot for security monitoring and spawner management - Implemented a bot that monitors for unauthorized players within a specified radius. - Added functionality to break spawners and stash items in an ender chest upon security trigger. - Integrated Discord webhook notifications for security alerts. - Configured bot to automatically arm security after joining the server. - Included logging for various actions and events for better tracking. --- index.js => bannable.js | 0 emergency_only.js | 530 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 530 insertions(+) rename index.js => bannable.js (100%) create mode 100644 emergency_only.js diff --git a/index.js b/bannable.js similarity index 100% rename from index.js rename to bannable.js diff --git a/emergency_only.js b/emergency_only.js new file mode 100644 index 0000000..3cbf286 --- /dev/null +++ b/emergency_only.js @@ -0,0 +1,530 @@ +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}`, +);