diff --git a/backend/src/data/DefaultLogMessages.json b/backend/src/data/DefaultLogMessages.json index dd992775..02b93494 100644 --- a/backend/src/data/DefaultLogMessages.json +++ b/backend/src/data/DefaultLogMessages.json @@ -51,6 +51,7 @@ "CASE_CREATE": "✏ {userMention(mod)} manually created new **{caseType}** case (#{caseNum})", "CASE_DELETE": "✂️ **Case #{case.case_number}** was deleted by {userMention(mod)}", + "MASSUNBAN": "⚒ {userMention(mod)} mass-unbanned {count} users", "MASSBAN": "⚒ {userMention(mod)} massbanned {count} users", "MASSMUTE": "📢🚫 {userMention(mod)} massmuted {count} users", diff --git a/backend/src/data/LogType.ts b/backend/src/data/LogType.ts index 14dadb69..60f6f9f7 100644 --- a/backend/src/data/LogType.ts +++ b/backend/src/data/LogType.ts @@ -39,6 +39,7 @@ export enum LogType { CASE_CREATE, + MASSUNBAN, MASSBAN, MASSMUTE, diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index 876ab044..993de951 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -17,6 +17,7 @@ import { SoftbanCmd } from "./commands/SoftbanCommand"; import { BanCmd } from "./commands/BanCmd"; import { UnbanCmd } from "./commands/UnbanCmd"; import { ForcebanCmd } from "./commands/ForcebanCmd"; +import { MassunbanCmd } from "./commands/MassUnbanCmd"; import { MassbanCmd } from "./commands/MassBanCmd"; import { AddCaseCmd } from "./commands/AddCaseCmd"; import { CaseCmd } from "./commands/CaseCmd"; @@ -67,6 +68,7 @@ const defaultOptions = { can_ban: false, can_view: false, can_addcase: false, + can_massunban: false, can_massban: false, can_massmute: false, can_hidecase: false, @@ -90,6 +92,7 @@ const defaultOptions = { { level: ">=100", config: { + can_massunban: true, can_massban: true, can_massmute: true, can_hidecase: true, @@ -134,6 +137,7 @@ export const ModActionsPlugin = zeppelinGuildPlugin()("mod ForcebanCmd, MassbanCmd, MassmuteCmd, + MassunbanCmd, AddCaseCmd, CaseCmd, CasesUserCmd, diff --git a/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts b/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts new file mode 100644 index 00000000..897cc25f --- /dev/null +++ b/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts @@ -0,0 +1,125 @@ +import { modActionsCmd, IgnoredEventType } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { stripObjectToScalars } from "../../../utils"; +import { isBanned } from "../functions/isBanned"; +import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; +import { CaseTypes } from "../../../data/CaseTypes"; +import { TextChannel } from "eris"; +import { waitForReply } from "knub/dist/helpers"; +import { ignoreEvent } from "../functions/ignoreEvent"; +import { CasesPlugin } from "../../Cases/CasesPlugin"; +import { LogType } from "../../../data/LogType"; + +export const MassunbanCmd = modActionsCmd({ + trigger: "massunban", + permission: "can_massunban", + description: "Mass-unban a list of user IDs", + + signature: [ + { + userIds: ct.string({ rest: true }), + }, + ], + + async run({ pluginData, message: msg, args }) { + // Limit to 100 users at once (arbitrary?) + if (args.userIds.length > 100) { + sendErrorMessage(pluginData, msg.channel, `Can only mass-unban max 100 users at once`); + return; + } + + // Ask for unban reason (cleaner this way instead of trying to cram it into the args) + msg.channel.createMessage("Unban reason? `cancel` to cancel"); + const unbanReasonReply = await waitForReply(pluginData.client, msg.channel as TextChannel, msg.author.id); + if (!unbanReasonReply || !unbanReasonReply.content || unbanReasonReply.content.toLowerCase().trim() === "cancel") { + sendErrorMessage(pluginData, msg.channel, "Cancelled"); + return; + } + + const unbanReason = formatReasonWithAttachments(unbanReasonReply.content, msg.attachments); + + // Ignore automatic unban cases and logs for these users + // We'll create our own cases below and post a single "mass unbanned" log instead + args.userIds.forEach(userId => { + // Use longer timeouts since this can take a while + ignoreEvent(pluginData, IgnoredEventType.Unban, userId, 120 * 1000); + pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, userId, 120 * 1000); + }); + + // Show a loading indicator since this can take a while + const loadingMsg = await msg.channel.createMessage("Unbanning..."); + + // Unban each user and count failed unbans (if any) + const failedUnbans: Array<{ userId: string; reason: UnbanFailReasons }> = []; + const casesPlugin = pluginData.getPlugin(CasesPlugin); + for (const userId of args.userIds) { + if (!(await isBanned(pluginData, userId))) { + failedUnbans.push({ userId, reason: UnbanFailReasons.NOT_BANNED }); + continue; + } + + try { + await pluginData.guild.unbanMember(userId, unbanReason != null ? encodeURIComponent(unbanReason) : undefined); + + await casesPlugin.createCase({ + userId, + modId: msg.author.id, + type: CaseTypes.Unban, + reason: `Mass unban: ${unbanReason}`, + postInCaseLogOverride: false, + }); + } catch (e) { + failedUnbans.push({ userId, reason: UnbanFailReasons.UNBAN_FAILED }); + } + } + + // Clear loading indicator + loadingMsg.delete(); + + const successfulUnbanCount = args.userIds.length - failedUnbans.length; + if (successfulUnbanCount === 0) { + // All unbans failed - don't create a log entry and notify the user + sendErrorMessage(pluginData, msg.channel, "All unbans failed. Make sure the IDs are valid and banned."); + } else { + // Some or all unbans were successful. Create a log entry for the mass unban and notify the user. + pluginData.state.serverLogs.log(LogType.MASSUNBAN, { + mod: stripObjectToScalars(msg.author), + count: successfulUnbanCount, + reason: unbanReason, + }); + + if (failedUnbans.length) { + const notBanned = failedUnbans.filter(x => x.reason === UnbanFailReasons.NOT_BANNED); + const unbanFailed = failedUnbans.filter(x => x.reason === UnbanFailReasons.UNBAN_FAILED); + + let failedMsg = ""; + if (notBanned.length > 0) { + failedMsg += `${notBanned.length}x ${UnbanFailReasons.NOT_BANNED}:`; + notBanned.forEach(fail => { + failedMsg += " " + fail.userId; + }); + } + if (unbanFailed.length > 0) { + failedMsg += `\n${unbanFailed.length}x ${UnbanFailReasons.UNBAN_FAILED}:`; + unbanFailed.forEach(fail => { + failedMsg += " " + fail.userId; + }); + } + + sendSuccessMessage( + pluginData, + msg.channel, + `Unbanned ${successfulUnbanCount} users, ${failedUnbans.length} failed:\n${failedMsg}`, + ); + } else { + sendSuccessMessage(pluginData, msg.channel, `Unbanned ${successfulUnbanCount} users successfully`); + } + } + }, +}); + +enum UnbanFailReasons { + NOT_BANNED = "Not banned", + UNBAN_FAILED = "Unban failed", +} diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index 4fb6ba5e..cf492480 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -35,6 +35,7 @@ export const ConfigSchema = t.type({ can_ban: t.boolean, can_view: t.boolean, can_addcase: t.boolean, + can_massunban: t.boolean, can_massban: t.boolean, can_massmute: t.boolean, can_hidecase: t.boolean,