diff --git a/index.mjs b/index.mjs index 14333fc..e381d02 100644 --- a/index.mjs +++ b/index.mjs @@ -42,7 +42,9 @@ if (config.default.debug.disableDiscord) { discordBot.setUtils(utils); discordBot.setConfig(config.default); } + refreshConfig(); +refreshBanRegistry(); // Used to refresh allowList every 10 seconds function refreshConfig() { @@ -61,6 +63,23 @@ function refreshConfig() { }, 10000); } +// Used to refresh banRegistry every 60 seconds +function refreshBanRegistry() { + setInterval(async () => { + try { + const now = Date.now(); + for (const [username, banData] of Object.entries(registry.bans)) { + if (banData.banEnd !== Infinity && banData.banEnd.getTime() <= now) { + delete registry.bans[username]; + console.log(`Ban for ${username} has expired and was removed.`); + } + } + } catch (error) { + console.error("Error refreshing ban registry:", error); + } + }, 60000); +} + process.stdin.on("data", dataInput); function dataInput(data) { diff --git a/src/mineflayer/commands/party/Ban.mjs b/src/mineflayer/commands/party/Ban.mjs index e3291ed..de82ae4 100644 --- a/src/mineflayer/commands/party/Ban.mjs +++ b/src/mineflayer/commands/party/Ban.mjs @@ -4,10 +4,14 @@ import { WebhookMessageType, } from "../../../utils/Interfaces.mjs"; +import { BanRegistry } from "../../../utils/Utils.mjs"; + +const registry = new BanRegistry(); + export default { name: ["ban", "block"], - description: "Ban a player from joining the party", - usage: "!p ban [reason]", + description: "Ban a player from joining the party, permanently or temporarily (durations in m, d, h, min)", + usage: "!p ban [duration] [reason]", permission: Permissions.Trusted, /** @@ -18,29 +22,102 @@ export default { */ execute: async function (bot, sender, args) { let player = args[0]; - if (!player) - return bot.reply( - sender, - `Invalid usage! Use: ${this.usage}`, - VerbosityLevel.Reduced, - ); - let reason = args.slice(1).join(" ") || "No reason given."; + if (!player) { + return bot.reply(sender, `Invalid usage! Use: ${this.usage}`, VerbosityLevel.Reduced); + } + const playerExists = await bot.utils.usernameExists(player); + if (playerExists === false) { + return bot.reply(sender, `Player ${player} not found`, VerbosityLevel.Reduced); + } + + let duration, reason; + if (args.length > 2) { + duration = args[1]; + reason = args.slice(2).join(" "); + } else if (args.length === 2) { + duration = 2592000000; + reason = args[1]; + } else { + duration = 2590002000; + reason = "No reason given."; + } + if (!bot.utils.isHigherRanked(sender.username, player)) { - return; + const senderPermsRank = bot.utils.getPermissionsByUser({ name: sender.username }); + const playerPermsRank = bot.utils.getPermissionsByUser({ name: player }); + const senderPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === senderPermsRank, + ); + const playerPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === playerPermsRank, + ); + if (senderPerms === undefined) return; + else if (senderPermsRank === playerPermsRank) { + return bot.reply( + sender, + `You cannot ban a player of the same permission level (both: ${senderPerms}).`, + VerbosityLevel.Reduced, + ); + } else { + return bot.reply( + sender, + `You cannot ban a player of a higher permission level than yourself (your rank: ${senderPerms} (level: ${senderPermsRank}), their rank: ${playerPerms} (level: ${playerPermsRank})).`, + VerbosityLevel.Reduced, + ); + } } - bot.chat( - `/pc ${player} was removed from the party and blocked from rejoining by ${sender.preferredName}.`, - ); + + bot.reply(sender, `Trying to ban ${player}...`, VerbosityLevel.Reduced); + + const remaining = getRemainingBanDuration(registry, player); + if (remaining && remaining !== "ban has expired") { + return bot.reply(sender, `This player is already banned, unban in ${remaining}.`, VerbosityLevel.Reduced); + } else if (remaining === "permanent ban") { + return bot.reply(sender, `This player is already banned permanently.`, VerbosityLevel.Reduced); + } + + const durationMillis = duration === 2592000000 ? 2592000000 : parseDuration(duration); + + if (durationMillis === null) { + return bot.reply(sender, `Could not parse the duration (${durationInput}). Please try again or use \`!p ban help\` for help.`, VerbosityLevel.Reduced); + } + + const banEndTimestamp = new Date(Date.now() + durationMillis); + registry.addBan(player, banEndTimestamp); + await bot.utils.delay(bot.utils.minMsgDelay); bot.chat(`/lobby`); await bot.utils.delay(bot.utils.minMsgDelay); + bot.chat(`/block add ${player}`); + await bot.utils.delay(bot.utils.minMsgDelay); bot.chat(`/p kick ${player}`); await bot.utils.delay(bot.utils.minMsgDelay); - bot.chat(`/block add ${player}`); + bot.reply(sender, `Banned ${player}.`, VerbosityLevel.Reduced); + + await bot.utils.delay(bot.utils.minMsgDelay); + bot.chat( + `/pc ${player} was removed from the party and blocked from rejoining by ${sender.preferredName}.`, + ); + bot.utils.webhookLogger.addMessage( `\`${player}\` was banned from the party by \`${sender.preferredName}\`. Reason: \`${reason}\``, WebhookMessageType.ActionLog, true, ); + + if (bot.utils.getUserObject({ name: player })) { + bot.utils.setPermissionRank({ + name: player, + newPermissionRank: 0, + }); + const newPlayerPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === newPermissionRank, + ); + bot.utils.webhookLogger.addMessage( + `After banning, \`${player}\`'s permission rank was updated to \`${newPlayerPerms}\` (level: \`${newPermissionRank}\`) by \`${sender.username}\`.`, + WebhookMessageType.ActionLog, + true, + ); + } }, }; diff --git a/src/mineflayer/commands/party/Kick.mjs b/src/mineflayer/commands/party/Kick.mjs index 1acf9bb..938d952 100644 --- a/src/mineflayer/commands/party/Kick.mjs +++ b/src/mineflayer/commands/party/Kick.mjs @@ -7,7 +7,7 @@ import { export default { name: ["kick", "remove"], description: "Kick someone from the party", - usage: "!p kick ", + usage: "!p kick [reason]", permission: Permissions.Trusted, /** @@ -17,26 +17,44 @@ export default { * @param {Array} args */ execute: async function (bot, sender, args) { - let player; - if (args[0]) { - player = await bot.utils.usernameExists(args[0]); - if (player === false) - return bot.reply(sender, "Player not found.", VerbosityLevel.Reduced); - } else - return bot.reply( - sender, - `Invalid usage! Use: ${this.usage}`, - VerbosityLevel.Reduced, - ); - let reason = args.slice(1).join(" ") || "No reason given."; + let player = args[0]; + if (!player) { + return bot.reply(sender, `Invalid usage! Use: ${this.usage}`, VerbosityLevel.Reduced); + } + const playerExists = await bot.utils.usernameExists(player); + if (playerExists === false) { + return bot.reply(sender, `Player ${player} not found.`, VerbosityLevel.Reduced); + } + const reason = args.slice(1).join(" ") || "No reason given."; + if (!bot.utils.isHigherRanked(sender.username, player)) { - return; + const senderPermsRank = bot.utils.getPermissionsByUser({ name: sender.username }); + const playerPermsRank = bot.utils.getPermissionsByUser({ name: player }); + const senderPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === senderPermsRank, + ); + const playerPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === playerPermsRank, + ); + if (senderPerms === undefined) return; + else { + return bot.reply( + sender, + `You cannot kick a player of a higher permission level than yourself (your rank: ${senderPerms} (level: ${senderPermsRank}), their rank: ${playerPerms} (level: ${playerPermsRank})).`, + VerbosityLevel.Reduced, + ); + } } + + await bot.utils.delay(bot.utils.MinMsgDelay); + bot.chat(`/lobby`); + await bot.utils.delay(bot.utils.MinMsgDelay); + bot.chat(`/p kick ${player}`); + await bot.utils.delay(bot.utils.minMsgDelay); bot.chat( `/pc ${player} was kicked from the party by ${sender.preferredName}.`, ); - await bot.utils.delay(bot.utils.minMsgDelay); - bot.chat(`/p kick ${player}`); + bot.utils.webhookLogger.addMessage( `\`${player}\` was kicked from the party by \`${sender.preferredName}\`. Reason: \`${reason}\``, WebhookMessageType.ActionLog, diff --git a/src/mineflayer/commands/party/Mute.mjs b/src/mineflayer/commands/party/Mute.mjs index 9e93481..80506b1 100644 --- a/src/mineflayer/commands/party/Mute.mjs +++ b/src/mineflayer/commands/party/Mute.mjs @@ -3,7 +3,7 @@ import { Permissions, WebhookMessageType } from "../../../utils/Interfaces.mjs"; export default { name: ["mute", "unmute"], description: "Mute/Unmute the party", - usage: "!p mute", + usage: "!p mute [reason]", permission: Permissions.Trusted, /** @@ -13,10 +13,12 @@ export default { * @param {Array} args */ execute: async function (bot, sender, args) { - let reason = args.join(" ") || "No reason given."; + const reason = args.join(" ") || "No reason given."; + bot.chat("/p mute"); await bot.utils.delay(bot.utils.minMsgDelay); bot.chat(`/pc Party mute was toggled by ${sender.preferredName}.`); + bot.utils.webhookLogger.addMessage( `Party mute was toggled by \`${sender.preferredName}\`. Reason: \`${reason}\``, WebhookMessageType.ActionLog, diff --git a/src/mineflayer/commands/party/Unban.mjs b/src/mineflayer/commands/party/Unban.mjs index 89325ed..b153ded 100644 --- a/src/mineflayer/commands/party/Unban.mjs +++ b/src/mineflayer/commands/party/Unban.mjs @@ -7,7 +7,7 @@ import { export default { name: ["unban", "unblock"], description: "Unban a player from the party", - usage: "!p unban ", + usage: "!p unban [reason]", permission: Permissions.Trusted, /** @@ -17,26 +17,62 @@ export default { * @param {Array} args */ execute: async function (bot, sender, args) { - let player; - if (args[0]) { - player = await bot.utils.usernameExists(args[0]); - if (player === false) - return bot.reply(sender, "Player not found.", VerbosityLevel.Reduced); - } else - return bot.reply( - sender, - `Invalid usage! Use: ${this.usage}`, - VerbosityLevel.Reduced, + let player = args[0]; + if (!player) { + return bot.reply(sender, `Invalid usage! Use: ${this.usage}`, VerbosityLevel.Reduced); + } + const playerExists = await bot.utils.usernameExists(player); + if (playerExists === false) { + return bot.reply(sender, `Player ${player} not found`, VerbosityLevel.Reduced); + } + const reason = args.slice(1).join(" ") || "No reason given."; + + if (!bot.utils.isHigherRanked(sender.username, player)) { + const senderPermsRank = bot.utils.getPermissionsByUser({ name: sender.username }); + const playerPermsRank = bot.utils.getPermissionsByUser({ name: player }); + const senderPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === senderPermsRank, + ); + const playerPerms = Object.keys(Permissions).find( + (perm) => Permissions[perm] === playerPermsRank, ); + if (senderPerms === undefined) return; + else if (senderPermsRank === playerPermsRank) { + return bot.reply( + sender, + `You cannot unban a player of the same permission level (both: ${senderPerms}).`, + VerbosityLevel.Reduced, + ); + } else { + return bot.reply( + sender, + `You cannot unban a player of a higher permission level than yourself (your rank: ${senderPerms} (level: ${senderPermsRank}), their rank: ${playerPerms} (level: ${playerPermsRank})).`, + VerbosityLevel.Reduced, + ); + } + } + bot.reply(sender, `Trying to unban ${player}...`, VerbosityLevel.Reduced); + + if (!registry.isBanned(username)) { + return bot.reply(sender, `${username} is not currently banned.`, VerbosityLevel.Reduced); + } else { + const remaining = getRemainingDuration(registry, player); + if (remaining && remaining !== "ban has expired") { + registry.removeBan(username); + return bot.reply(sender, `${username}'s ban has been reduced from ${remaining} to unbanned.`, VerbosityLevel.Reduced); + } + } + await bot.utils.delay(bot.utils.minMsgDelay); bot.chat(`/lobby`); await bot.utils.delay(bot.utils.minMsgDelay); bot.chat(`/block remove ${player}`); await bot.utils.delay(bot.utils.minMsgDelay); bot.reply(sender, `Unbanned ${player}.`, VerbosityLevel.Reduced); + bot.utils.webhookLogger.addMessage( - `\`${player}\` was unbanned from the party by \`${sender.preferredName}\`.`, + `\`${player}\` was unbanned from the party by \`${sender.preferredName}\`. Reason: \`${reason}\``, WebhookMessageType.ActionLog, true, ); diff --git a/src/utils/Utils.mjs b/src/utils/Utils.mjs index 5d9715d..ffab09e 100644 --- a/src/utils/Utils.mjs +++ b/src/utils/Utils.mjs @@ -16,6 +16,7 @@ class Utils { this.refreshKickList(); // Turn on kickList refreshing this.rulesList = rulesList; // Set rulesList this.refreshRulesList(); // Turn on rulesList refreshing + this.banRegistry(); // Set banRegistry this.link = new Link(); // Set Link class this.discordReply = new DiscordReply(); // Set DiscordReply class this.webhookLogger = new WebhookLogger(); // Set WebhookLogger class @@ -863,6 +864,83 @@ class Debug { } } +class BanRegistry { + constructor() { + this.bans = {}; + } + + /** + * Adds or updates a ban entry for a user. + * @param {string} username - The user's name. + * @param {Date|number} banEnd - The ban end as a Date object or timestamp (ms). + */ + addBan(username, banEnd) { + this.bans[username] = { + banStart: new Date(), + banEnd: banEnd instanceof Date ? banEnd : new Date(banEnd), + }; + } + + removeBan(username) { + delete this.bans[username]; + } + + isBanned(username) { + return !!this.bans[username]; + } + + parseDuration(input) { + if (typeof input !== "string") return null; + + const regex = /^(?:(\d+)\s*m(?!.*m))?(?:(\d+)\s*d(?!.*d))?(?:(\d+)\s*h(?!.*h))?(?:(\d+)\s*min(?!.*min))?$/gi; + let match; + let totalMillis = 0; + + while ((match = regex.exec(input)) !== null) { + const value = parseInt(match[1]); + const unit = match[2].toLowerCase(); + + switch (unit) { + case "d": + totalMs += value * 24 * 60 * 60 * 1000; + break; + case "h": + totalMs += value * 60 * 60 * 1000; + break; + case "m": + case "min": + totalMs += value * 60 * 1000; + break; + } + } + return totalMs || null; + } + + getRemainingBanDuration(registry, username) { + const banData = registry.bans[username]; + if (!banData) return null; + + if (banData.banEnd === Infinity) return "permanent ban"; + + const now = Date.now(); + const diffMillis = banData.banEnd.getTime() - now; + if (diffMillis <= 0) return "ban has expired"; + + const seconds = Math.floor(diffMillis / 1000) % 60; + const minutes = Math.floor(diffMillis / (1000 * 60)) % 60; + const hours = Math.floor(diffMillis / (1000 * 60 * 60)) % 24; + const days = Math.floor(diffMillis / (1000 * 60 * 60 * 24)); + + let parts = []; + if (days) parts.push(`${days} day${days !== 1 ? "s" : ""}`); + if (hours) parts.push(`${hours} hour${hours !== 1 ? "s" : ""}`); + if (minutes) parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`); + if (seconds) parts.push(`${seconds} second${seconds !== 1 ? "s" : ""}`); + + return parts.length ? parts.join(", ") : "less than a second"; + } +} + class Link { constructor() { this.collection = new Collection();