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.
This commit is contained in:
parent
b347e0a984
commit
ff255bea38
530
emergency_only.js
Normal file
530
emergency_only.js
Normal file
@ -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}`,
|
||||
);
|
||||
Loading…
x
Reference in New Issue
Block a user