From 4410f20562ab37a15352bfbcdae947dd91aae96b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:12:19 +0300 Subject: [PATCH 01/12] Improve invite detection Several less common patterns are now detected: discord.gg/anything/here/ discord.com/invite//anything/here discordapp.com/invite//anything/here Potential invite URLs are also parsed as URLs to clean out any shenanigans related to valid-but-uncommon URL formats. --- backend/src/utils.ts | 62 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 98ec406f..059ddac9 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -20,7 +20,7 @@ import { TextChannel, User, } from "eris"; -import url from "url"; +import { URL } from "url"; import tlds from "tlds"; import emojiRegex from "emoji-regex"; import * as t from "io-ts"; @@ -481,7 +481,7 @@ const plainLinkRegex = /((?!https?:\/\/)\S)+\.\S+/; // anything.anything, withou const urlRegex = new RegExp(`(${realLinkRegex.source}|${plainLinkRegex.source})`, "g"); const protocolRegex = /^[a-z]+:\/\//; -interface MatchedURL extends url.URL { +interface MatchedURL extends URL { input: string; } @@ -496,7 +496,7 @@ export function getUrlsInString(str: string, onlyUnique = false): MatchedURL[] { let matchUrl: MatchedURL; try { - matchUrl = new url.URL(withProtocol) as MatchedURL; + matchUrl = new URL(withProtocol) as MatchedURL; matchUrl.input = match; } catch (e) { return urls; @@ -520,9 +520,61 @@ export function parseInviteCodeInput(str: string): string { return getInviteCodesInString(str)[0]; } +export function isNotNull(value): value is Exclude { + return value != null; +} + +// discord.com/invite/ +// discordapp.com/invite/ +// discord.gg/invite/ +// discord.gg/ +const quickInviteDetection = /(?:discord.com|discordapp.com)\/invite\/([^\s\/#?]+)|discord.gg\/(?:\S+\/)?([^\s\/#?]+)/gi; + +const isInviteHostRegex = /(?:^|\.)(?:discord.gg|discord.com|discordapp.com)$/; +const longInvitePathRegex = /^\/invite\/([^\s\/]+)$/; + export function getInviteCodesInString(str: string): string[] { - const inviteCodeRegex = /(?:discord.gg|discordapp.com\/invite|discord.com\/invite)\/([a-z0-9\-]+)/gi; - return Array.from(str.matchAll(inviteCodeRegex)).map(m => m[1]); + const inviteCodes: string[] = []; + + // Clean up markdown + str = str.replace(/[|*_~]/g, ""); + + // Quick detection + const quickDetectionMatch = str.matchAll(quickInviteDetection); + if (quickDetectionMatch) { + inviteCodes.push(...[...quickDetectionMatch].map(m => m[1] || m[2])); + } + + // Deep detection via URL parsing + const linksInString = getUrlsInString(str, true); + const potentialInviteLinks = linksInString.filter(url => isInviteHostRegex.test(url.hostname)); + const withNormalizedPaths = potentialInviteLinks.map(url => { + url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/+$/g, ""); + return url; + }); + + const codesFromInviteLinks = withNormalizedPaths + .map(url => { + // discord.gg/[anything/] + if (url.hostname === "discord.gg") { + const parts = url.pathname.split("/").filter(Boolean); + return parts[parts.length - 1]; + } + + // discord.com/invite/[/anything] + // discordapp.com/invite/[/anything] + const longInviteMatch = url.pathname.match(longInvitePathRegex); + if (longInviteMatch) { + return longInviteMatch[1]; + } + + return null; + }) + .filter(Boolean) as string[]; + + inviteCodes.push(...codesFromInviteLinks); + + return unique(inviteCodes); } export const unicodeEmojiRegex = emojiRegex(); From d747ac39829eb5edc390e2397d776662dce94a13 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:23:51 +0300 Subject: [PATCH 02/12] Optimize isBanned() and handle errors properly --- .../plugins/ModActions/functions/isBanned.ts | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/backend/src/plugins/ModActions/functions/isBanned.ts b/backend/src/plugins/ModActions/functions/isBanned.ts index c809a30c..7bcd22d8 100644 --- a/backend/src/plugins/ModActions/functions/isBanned.ts +++ b/backend/src/plugins/ModActions/functions/isBanned.ts @@ -1,16 +1,44 @@ import { GuildPluginData } from "knub"; import { ModActionsPluginType } from "../types"; -import { isDiscordHTTPError } from "../../../utils"; +import { isDiscordHTTPError, isDiscordRESTError, SECONDS, sleep } from "../../../utils"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { LogType } from "../../../data/LogType"; +import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; +import { Constants } from "eris"; + +export async function isBanned( + pluginData: GuildPluginData, + userId: string, + timeout: number = 5 * SECONDS, +): Promise { + const botMember = pluginData.guild.members.get(pluginData.client.user.id); + if (botMember && !hasDiscordPermissions(botMember.permissions, Constants.Permissions.banMembers)) { + pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, { + body: `Missing "Ban Members" permission to check for existing bans`, + }); + return false; + } -export async function isBanned(pluginData: GuildPluginData, userId: string): Promise { try { - const bans = await pluginData.guild.getBans(); - return bans.some(b => b.user.id === userId); + const potentialBan = await Promise.race([pluginData.guild.getBan(userId), sleep(timeout)]); + return potentialBan != null; } catch (e) { - if (isDiscordHTTPError(e) && e.code === 500) { + if (isDiscordRESTError(e) && e.code === 10026) { + // [10026]: Unknown Ban return false; } + if (isDiscordHTTPError(e) && e.code === 500) { + // Internal server error, ignore + return false; + } + + if (isDiscordRESTError(e) && e.code === 50013) { + pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, { + body: `Missing "Ban Members" permission to check for existing bans`, + }); + } + throw e; } } From c7c1b1f96f2b6241f54476d8e148069c1c3d6770 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:24:56 +0300 Subject: [PATCH 03/12] !ban: only run isBanned() if member is not found --- backend/src/plugins/ModActions/commands/BanCmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts index da7156c2..f42b426a 100644 --- a/backend/src/plugins/ModActions/commands/BanCmd.ts +++ b/backend/src/plugins/ModActions/commands/BanCmd.ts @@ -66,8 +66,8 @@ export const BanCmd = modActionsCmd({ const lock = await pluginData.locks.acquire(banLock(user)); let forceban = false; const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id); - const banned = await isBanned(pluginData, user.id); if (!memberToBan) { + const banned = await isBanned(pluginData, user.id); if (banned) { // Abort if trying to ban user indefinitely if they are already banned indefinitely if (!existingTempban && !time) { From ee615c1f0e527662b77341d5226a2b4fefbd0d04 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:44:15 +0300 Subject: [PATCH 04/12] Optimize !search and !bansearch performance for non-regex searches --- backend/src/plugins/Utility/search.ts | 62 +++++++++++++++++---------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index c369e7ba..682765ee 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -10,7 +10,7 @@ import { banSearchSignature } from "./commands/BanSearchCmd"; import { UtilityPluginType } from "./types"; import { refreshMembersIfNeeded } from "./refreshMembers"; import { getUserInfoEmbed } from "./functions/getUserInfoEmbed"; -import { allowTimeout } from "../../RegExpRunner"; +import { allowTimeout, RegExpRunner } from "../../RegExpRunner"; import { inputPatternToRegExp, InvalidRegexError } from "../../validatorUtils"; import { asyncFilter } from "../../utils/async"; import Timeout = NodeJS.Timeout; @@ -29,6 +29,29 @@ class SearchError extends Error {} type MemberSearchParams = ArgsFromSignatureOrArray; type BanSearchParams = ArgsFromSignatureOrArray; +type RegexRunner = InstanceType["exec"]; +function getOptimizedRegExpRunner(pluginData: GuildPluginData, isSafeRegex: boolean): RegexRunner { + if (isSafeRegex) { + return async (regex: RegExp, str: string) => { + if (!regex.global) { + const singleMatch = regex.exec(str); + return singleMatch ? [singleMatch] : null; + } + + const matches: RegExpExecArray[] = []; + let match: RegExpExecArray | null; + // tslint:disable-next-line:no-conditional-assignment + while ((match = regex.exec(str)) != null) { + matches.push(match); + } + + return matches.length ? matches : null; + }; + } + + return pluginData.state.regexRunner.exec.bind(pluginData.state.regexRunner); +} + export async function displaySearch( pluginData: GuildPluginData, args: MemberSearchParams, @@ -270,59 +293,51 @@ async function performMemberSearch( } if (args.query) { + let isSafeRegex = true; let queryRegex: RegExp; if (args.regex) { const flags = args["case-sensitive"] ? "" : "i"; queryRegex = inputPatternToRegExp(args.query.trimStart()); queryRegex = new RegExp(queryRegex.source, flags); + isSafeRegex = false; } else { queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "" : "i"); } + const execRegExp = getOptimizedRegExpRunner(pluginData, isSafeRegex); + if (args["status-search"]) { matchingMembers = await asyncFilter(matchingMembers, async member => { if (member.game) { - if ( - member.game.name && - (await pluginData.state.regexRunner.exec(queryRegex, member.game.name).catch(allowTimeout)) - ) { + if (member.game.name && (await execRegExp(queryRegex, member.game.name).catch(allowTimeout))) { return true; } - if ( - member.game.state && - (await pluginData.state.regexRunner.exec(queryRegex, member.game.state).catch(allowTimeout)) - ) { + if (member.game.state && (await execRegExp(queryRegex, member.game.state).catch(allowTimeout))) { return true; } - if ( - member.game.details && - (await pluginData.state.regexRunner.exec(queryRegex, member.game.details).catch(allowTimeout)) - ) { + if (member.game.details && (await execRegExp(queryRegex, member.game.details).catch(allowTimeout))) { return true; } if (member.game.assets) { if ( member.game.assets.small_text && - (await pluginData.state.regexRunner.exec(queryRegex, member.game.assets.small_text).catch(allowTimeout)) + (await execRegExp(queryRegex, member.game.assets.small_text).catch(allowTimeout)) ) { return true; } if ( member.game.assets.large_text && - (await pluginData.state.regexRunner.exec(queryRegex, member.game.assets.large_text).catch(allowTimeout)) + (await execRegExp(queryRegex, member.game.assets.large_text).catch(allowTimeout)) ) { return true; } } - if ( - member.game.emoji && - (await pluginData.state.regexRunner.exec(queryRegex, member.game.emoji.name).catch(allowTimeout)) - ) { + if (member.game.emoji && (await execRegExp(queryRegex, member.game.emoji.name).catch(allowTimeout))) { return true; } } @@ -330,12 +345,12 @@ async function performMemberSearch( }); } else { matchingMembers = await asyncFilter(matchingMembers, async member => { - if (member.nick && (await pluginData.state.regexRunner.exec(queryRegex, member.nick).catch(allowTimeout))) { + if (member.nick && (await execRegExp(queryRegex, member.nick).catch(allowTimeout))) { return true; } const fullUsername = `${member.user.username}#${member.user.discriminator}`; - if (await pluginData.state.regexRunner.exec(queryRegex, fullUsername).catch(allowTimeout)) return true; + if (await execRegExp(queryRegex, fullUsername).catch(allowTimeout)) return true; return false; }); @@ -383,18 +398,21 @@ async function performBanSearch( let matchingBans = (await pluginData.guild.getBans()).map(x => x.user); if (args.query) { + let isSafeRegex = true; let queryRegex: RegExp; if (args.regex) { const flags = args["case-sensitive"] ? "" : "i"; queryRegex = inputPatternToRegExp(args.query.trimStart()); queryRegex = new RegExp(queryRegex.source, flags); + isSafeRegex = false; } else { queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "" : "i"); } + const execRegExp = getOptimizedRegExpRunner(pluginData, isSafeRegex); matchingBans = await asyncFilter(matchingBans, async user => { const fullUsername = `${user.username}#${user.discriminator}`; - if (await pluginData.state.regexRunner.exec(queryRegex, fullUsername).catch(allowTimeout)) return true; + if (await execRegExp(queryRegex, fullUsername).catch(allowTimeout)) return true; return false; }); } From aa43f05173205fb600e8a44e325f76b5c2ca8bfe Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:47:45 +0300 Subject: [PATCH 05/12] Check !bansearch permissions before running search --- backend/src/plugins/Utility/search.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 682765ee..ec7a6604 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -1,4 +1,4 @@ -import { Member, Message, User } from "eris"; +import { Constants, Member, Message, User } from "eris"; import moment from "moment-timezone"; import escapeStringRegexp from "escape-string-regexp"; import { isFullMessage, MINUTES, multiSorter, noop, sorter, trimLines } from "../../utils"; @@ -14,6 +14,7 @@ import { allowTimeout, RegExpRunner } from "../../RegExpRunner"; import { inputPatternToRegExp, InvalidRegexError } from "../../validatorUtils"; import { asyncFilter } from "../../utils/async"; import Timeout = NodeJS.Timeout; +import { hasDiscordPermissions } from "../../utils/hasDiscordPermissions"; const SEARCH_RESULTS_PER_PAGE = 15; const SEARCH_ID_RESULTS_PER_PAGE = 50; @@ -395,6 +396,11 @@ async function performBanSearch( page = 1, perPage = SEARCH_RESULTS_PER_PAGE, ): Promise<{ results: User[]; totalResults: number; page: number; lastPage: number; from: number; to: number }> { + const member = pluginData.guild.members.get(pluginData.client.user.id); + if (member && !hasDiscordPermissions(member.permissions, Constants.Permissions.banMembers)) { + throw new SearchError(`Unable to search bans: missing "Ban Members" permission`); + } + let matchingBans = (await pluginData.guild.getBans()).map(x => x.user); if (args.query) { From 4448659dc0a7ceaff3028f3373ba76c2b07dfa88 Mon Sep 17 00:00:00 2001 From: Almeida Date: Wed, 28 Apr 2021 19:59:56 +0100 Subject: [PATCH 06/12] fix(SetCounterCmd): misleading messages (#188) --- backend/src/plugins/Counters/commands/SetCounterCmd.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/Counters/commands/SetCounterCmd.ts b/backend/src/plugins/Counters/commands/SetCounterCmd.ts index edabe2e7..0503b2fd 100644 --- a/backend/src/plugins/Counters/commands/SetCounterCmd.ts +++ b/backend/src/plugins/Counters/commands/SetCounterCmd.ts @@ -67,7 +67,7 @@ export const SetCounterCmd = guildCommand()({ let channel = args.channel; if (!channel && counter.per_channel) { - message.channel.createMessage(`Which channel's counter value would you like to add to?`); + message.channel.createMessage(`Which channel's counter value would you like to change?`); const reply = await waitForReply(pluginData.client, message.channel, message.author.id); if (!reply || !reply.content) { sendErrorMessage(pluginData, message.channel, "Cancelling"); @@ -85,7 +85,7 @@ export const SetCounterCmd = guildCommand()({ let user = args.user; if (!user && counter.per_user) { - message.channel.createMessage(`Which user's counter value would you like to add to?`); + message.channel.createMessage(`Which user's counter value would you like to change?`); const reply = await waitForReply(pluginData.client, message.channel, message.author.id); if (!reply || !reply.content) { sendErrorMessage(pluginData, message.channel, "Cancelling"); @@ -103,7 +103,7 @@ export const SetCounterCmd = guildCommand()({ let value = args.value; if (!value) { - message.channel.createMessage("How much would you like to add to the counter's value?"); + message.channel.createMessage("What would you like to set the counter's value to?"); const reply = await waitForReply(pluginData.client, message.channel, message.author.id); if (!reply || !reply.content) { sendErrorMessage(pluginData, message.channel, "Cancelling"); From 00f368d62bab6a73a780b6fea6590c0620c89ca0 Mon Sep 17 00:00:00 2001 From: Shoaib Sajid Date: Thu, 29 Apr 2021 00:02:25 +0500 Subject: [PATCH 07/12] Fix typo in automod docs (#185) --- backend/src/plugins/Automod/info.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/Automod/info.ts b/backend/src/plugins/Automod/info.ts index c2c87dbf..9fcdced0 100644 --- a/backend/src/plugins/Automod/info.ts +++ b/backend/src/plugins/Automod/info.ts @@ -64,9 +64,9 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { reason: 'Auto-muted for spam' my_second_filter: triggers: - - message_spam: - amount: 5 - within: 10s + - emoji_spam: + amount: 2 + within: 5s actions: clean: true overrides: From 6b9131c353a578eb3af839a620f7edd9e4072306 Mon Sep 17 00:00:00 2001 From: Usoka <27248545+Usoka@users.noreply.github.com> Date: Thu, 29 Apr 2021 07:03:26 +1200 Subject: [PATCH 08/12] Fix 0 not being accepted in SetCounterCmd (#186) --- backend/src/plugins/Counters/commands/SetCounterCmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Counters/commands/SetCounterCmd.ts b/backend/src/plugins/Counters/commands/SetCounterCmd.ts index 0503b2fd..f74c2045 100644 --- a/backend/src/plugins/Counters/commands/SetCounterCmd.ts +++ b/backend/src/plugins/Counters/commands/SetCounterCmd.ts @@ -111,7 +111,7 @@ export const SetCounterCmd = guildCommand()({ } const potentialValue = parseInt(reply.content, 10); - if (!potentialValue) { + if (Number.isNaN(potentialValue)) { sendErrorMessage(pluginData, message.channel, "Not a number, cancelling"); return; } From 90b6f4bc86d6e5756dbcc97956cfc05b99176643 Mon Sep 17 00:00:00 2001 From: Shoaib Sajid Date: Thu, 29 Apr 2021 00:04:01 +0500 Subject: [PATCH 09/12] Add !reminder as an alias for !remind (#181) --- backend/src/plugins/Reminders/commands/RemindCmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Reminders/commands/RemindCmd.ts b/backend/src/plugins/Reminders/commands/RemindCmd.ts index 67a2cfc1..ad9cd355 100644 --- a/backend/src/plugins/Reminders/commands/RemindCmd.ts +++ b/backend/src/plugins/Reminders/commands/RemindCmd.ts @@ -7,7 +7,7 @@ import { remindersCmd } from "../types"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export const RemindCmd = remindersCmd({ - trigger: ["remind", "remindme"], + trigger: ["remind", "remindme", "reminder"], usage: "!remind 3h Remind me of this in 3 hours please", permission: "can_use", From 51db942d977cfae2b0411248e0991d30b0b398cb Mon Sep 17 00:00:00 2001 From: Nils <7890309+DarkView@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:06:33 +0200 Subject: [PATCH 10/12] Allow Automod to distinguish whether mod actions are manual or automatic (#179) --- backend/src/plugins/Automod/AutomodPlugin.ts | 24 +++++++++++++------ backend/src/plugins/Automod/actions/ban.ts | 7 +++++- backend/src/plugins/Automod/actions/kick.ts | 2 +- backend/src/plugins/Automod/actions/mute.ts | 9 ++++++- backend/src/plugins/Automod/actions/warn.ts | 2 +- .../Automod/events/runAutomodOnModAction.ts | 2 ++ backend/src/plugins/Automod/triggers/ban.ts | 18 +++++++++++--- backend/src/plugins/Automod/triggers/kick.ts | 17 ++++++++++--- backend/src/plugins/Automod/triggers/mute.ts | 17 ++++++++++--- backend/src/plugins/Automod/triggers/warn.ts | 17 ++++++++++--- backend/src/plugins/Automod/types.ts | 1 + .../plugins/ModActions/commands/WarnCmd.ts | 2 -- .../plugins/ModActions/functions/banUserId.ts | 2 +- .../ModActions/functions/kickMember.ts | 2 +- .../ModActions/functions/warnMember.ts | 2 ++ backend/src/plugins/ModActions/types.ts | 9 ++++--- .../src/plugins/Mutes/functions/muteUser.ts | 2 +- backend/src/plugins/Mutes/types.ts | 3 ++- 18 files changed, 106 insertions(+), 32 deletions(-) diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index 7bebd606..2a6cf870 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -235,14 +235,20 @@ export const AutomodPlugin = zeppelinGuildPlugin()("automod", pluginData.state.modActionsListeners.set("note", (userId: string) => runAutomodOnModAction(pluginData, "note", userId), ); - pluginData.state.modActionsListeners.set("warn", (userId: string) => - runAutomodOnModAction(pluginData, "warn", userId), + pluginData.state.modActionsListeners.set( + "warn", + (userId: string, reason: string | undefined, isAutomodAction: boolean) => + runAutomodOnModAction(pluginData, "warn", userId, reason, isAutomodAction), ); - pluginData.state.modActionsListeners.set("kick", (userId: string) => - runAutomodOnModAction(pluginData, "kick", userId), + pluginData.state.modActionsListeners.set( + "kick", + (userId: string, reason: string | undefined, isAutomodAction: boolean) => + runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction), ); - pluginData.state.modActionsListeners.set("ban", (userId: string) => - runAutomodOnModAction(pluginData, "ban", userId), + pluginData.state.modActionsListeners.set( + "ban", + (userId: string, reason: string | undefined, isAutomodAction: boolean) => + runAutomodOnModAction(pluginData, "ban", userId, reason, isAutomodAction), ); pluginData.state.modActionsListeners.set("unban", (userId: string) => runAutomodOnModAction(pluginData, "unban", userId), @@ -251,7 +257,11 @@ export const AutomodPlugin = zeppelinGuildPlugin()("automod", const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter(); pluginData.state.mutesListeners = new Map(); - pluginData.state.mutesListeners.set("mute", (userId: string) => runAutomodOnModAction(pluginData, "mute", userId)); + pluginData.state.mutesListeners.set( + "mute", + (userId: string, reason: string | undefined, isAutomodAction: boolean) => + runAutomodOnModAction(pluginData, "mute", userId, reason, isAutomodAction), + ); pluginData.state.mutesListeners.set("unmute", (userId: string) => runAutomodOnModAction(pluginData, "unmute", userId), ); diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index 99b6c25b..ab9e811a 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -33,7 +33,12 @@ export const BanAction = automodAction({ const modActions = pluginData.getPlugin(ModActionsPlugin); for (const userId of userIdsToBan) { - await modActions.banUserId(userId, reason, { contactMethods, caseArgs, deleteMessageDays }); + await modActions.banUserId(userId, reason, { + contactMethods, + caseArgs, + deleteMessageDays, + isAutomodAction: true, + }); } }, }); diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts index c25684f4..b11c44ae 100644 --- a/backend/src/plugins/Automod/actions/kick.ts +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -33,7 +33,7 @@ export const KickAction = automodAction({ const modActions = pluginData.getPlugin(ModActionsPlugin); for (const member of membersToKick) { if (!member) continue; - await modActions.kickMember(member, reason, { contactMethods, caseArgs }); + await modActions.kickMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true }); } }, }); diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts index ad964f44..f09fbe34 100644 --- a/backend/src/plugins/Automod/actions/mute.ts +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -49,7 +49,14 @@ export const MuteAction = automodAction({ const mutes = pluginData.getPlugin(MutesPlugin); for (const userId of userIdsToMute) { try { - await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs }, rolesToRemove, rolesToRestore); + await mutes.muteUser( + userId, + duration, + reason, + { contactMethods, caseArgs, isAutomodAction: true }, + rolesToRemove, + rolesToRestore, + ); } catch (e) { if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) { pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, { diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts index c705b77c..70240080 100644 --- a/backend/src/plugins/Automod/actions/warn.ts +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -33,7 +33,7 @@ export const WarnAction = automodAction({ const modActions = pluginData.getPlugin(ModActionsPlugin); for (const member of membersToWarn) { if (!member) continue; - await modActions.warnMember(member, reason, { contactMethods, caseArgs }); + await modActions.warnMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true }); } }, }); diff --git a/backend/src/plugins/Automod/events/runAutomodOnModAction.ts b/backend/src/plugins/Automod/events/runAutomodOnModAction.ts index 5831f1eb..59b2b650 100644 --- a/backend/src/plugins/Automod/events/runAutomodOnModAction.ts +++ b/backend/src/plugins/Automod/events/runAutomodOnModAction.ts @@ -9,6 +9,7 @@ export async function runAutomodOnModAction( modAction: ModActionType, userId: string, reason?: string, + isAutomodAction: boolean = false, ) { const user = await resolveUser(pluginData.client, userId); @@ -18,6 +19,7 @@ export async function runAutomodOnModAction( modAction: { type: modAction, reason, + isAutomodAction, }, }; diff --git a/backend/src/plugins/Automod/triggers/ban.ts b/backend/src/plugins/Automod/triggers/ban.ts index 64559e74..a7c16742 100644 --- a/backend/src/plugins/Automod/triggers/ban.ts +++ b/backend/src/plugins/Automod/triggers/ban.ts @@ -5,13 +5,25 @@ import { automodTrigger } from "../helpers"; interface BanTriggerResultType {} export const BanTrigger = automodTrigger()({ - configType: t.type({}), - defaultConfig: {}, + configType: t.type({ + manual: t.boolean, + automatic: t.boolean, + }), - async match({ context }) { + defaultConfig: { + manual: true, + automatic: true, + }, + + async match({ context, triggerConfig }) { if (context.modAction?.type !== "ban") { return; } + console.log(context); + // If automatic && automatic turned off -> return + if (context.modAction.isAutomodAction && !triggerConfig.automatic) return; + // If manual && manual turned off -> return + if (!context.modAction.isAutomodAction && !triggerConfig.manual) return; return { extra: {}, diff --git a/backend/src/plugins/Automod/triggers/kick.ts b/backend/src/plugins/Automod/triggers/kick.ts index 284a867d..116f1252 100644 --- a/backend/src/plugins/Automod/triggers/kick.ts +++ b/backend/src/plugins/Automod/triggers/kick.ts @@ -5,13 +5,24 @@ import { automodTrigger } from "../helpers"; interface KickTriggerResultType {} export const KickTrigger = automodTrigger()({ - configType: t.type({}), - defaultConfig: {}, + configType: t.type({ + manual: t.boolean, + automatic: t.boolean, + }), - async match({ context }) { + defaultConfig: { + manual: true, + automatic: true, + }, + + async match({ context, triggerConfig }) { if (context.modAction?.type !== "kick") { return; } + // If automatic && automatic turned off -> return + if (context.modAction.isAutomodAction && !triggerConfig.automatic) return; + // If manual && manual turned off -> return + if (!context.modAction.isAutomodAction && !triggerConfig.manual) return; return { extra: {}, diff --git a/backend/src/plugins/Automod/triggers/mute.ts b/backend/src/plugins/Automod/triggers/mute.ts index 94d14437..c5e2d2ba 100644 --- a/backend/src/plugins/Automod/triggers/mute.ts +++ b/backend/src/plugins/Automod/triggers/mute.ts @@ -5,13 +5,24 @@ import { automodTrigger } from "../helpers"; interface MuteTriggerResultType {} export const MuteTrigger = automodTrigger()({ - configType: t.type({}), - defaultConfig: {}, + configType: t.type({ + manual: t.boolean, + automatic: t.boolean, + }), - async match({ context }) { + defaultConfig: { + manual: true, + automatic: true, + }, + + async match({ context, triggerConfig }) { if (context.modAction?.type !== "mute") { return; } + // If automatic && automatic turned off -> return + if (context.modAction.isAutomodAction && !triggerConfig.automatic) return; + // If manual && manual turned off -> return + if (!context.modAction.isAutomodAction && !triggerConfig.manual) return; return { extra: {}, diff --git a/backend/src/plugins/Automod/triggers/warn.ts b/backend/src/plugins/Automod/triggers/warn.ts index 711f5cd7..545c4437 100644 --- a/backend/src/plugins/Automod/triggers/warn.ts +++ b/backend/src/plugins/Automod/triggers/warn.ts @@ -5,13 +5,24 @@ import { automodTrigger } from "../helpers"; interface WarnTriggerResultType {} export const WarnTrigger = automodTrigger()({ - configType: t.type({}), - defaultConfig: {}, + configType: t.type({ + manual: t.boolean, + automatic: t.boolean, + }), - async match({ context }) { + defaultConfig: { + manual: true, + automatic: true, + }, + + async match({ context, triggerConfig }) { if (context.modAction?.type !== "warn") { return; } + // If automatic && automatic turned off -> return + if (context.modAction.isAutomodAction && !triggerConfig.automatic) return; + // If manual && manual turned off -> return + if (!context.modAction.isAutomodAction && !triggerConfig.manual) return; return { extra: {}, diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index f85b8429..cd7ebacf 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -122,6 +122,7 @@ export interface AutomodContext { modAction?: { type: ModActionType; reason?: string; + isAutomodAction: boolean; }; antiraid?: { level: string | null; diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index e36dbb86..45526a0c 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -112,7 +112,5 @@ export const WarnCmd = modActionsCmd({ msg.channel, `Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${warnResult.case.case_number})${messageResultText}`, ); - - pluginData.state.events.emit("warn", user.id, reason); }, }); diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index 85d05de9..4620ec0f 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -127,7 +127,7 @@ export async function banUserId( banTime: banTime ? humanizeDuration(banTime) : null, }); - pluginData.state.events.emit("ban", user.id, reason); + pluginData.state.events.emit("ban", user.id, reason, banOptions.isAutomodAction); return { status: "success", diff --git a/backend/src/plugins/ModActions/functions/kickMember.ts b/backend/src/plugins/ModActions/functions/kickMember.ts index 9e297af8..b682018e 100644 --- a/backend/src/plugins/ModActions/functions/kickMember.ts +++ b/backend/src/plugins/ModActions/functions/kickMember.ts @@ -85,7 +85,7 @@ export async function kickMember( reason, }); - pluginData.state.events.emit("kick", member.id, reason); + pluginData.state.events.emit("kick", member.id, reason, kickOptions.isAutomodAction); return { status: "success", diff --git a/backend/src/plugins/ModActions/functions/warnMember.ts b/backend/src/plugins/ModActions/functions/warnMember.ts index 0c07c838..d61b183d 100644 --- a/backend/src/plugins/ModActions/functions/warnMember.ts +++ b/backend/src/plugins/ModActions/functions/warnMember.ts @@ -82,6 +82,8 @@ export async function warnMember( reason, }); + pluginData.state.events.emit("warn", member.id, reason, warnOptions.isAutomodAction); + return { status: "success", case: createdCase, diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index af4fd8d6..083e5642 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -48,9 +48,9 @@ export type TConfigSchema = t.TypeOf; export interface ModActionsEvents { note: (userId: string, reason?: string) => void; - warn: (userId: string, reason?: string) => void; - kick: (userId: string, reason?: string) => void; - ban: (userId: string, reason?: string) => void; + warn: (userId: string, reason?: string, isAutomodAction?: boolean) => void; + kick: (userId: string, reason?: string, isAutomodAction?: boolean) => void; + ban: (userId: string, reason?: string, isAutomodAction?: boolean) => void; unban: (userId: string, reason?: string) => void; // mute/unmute are in the Mutes plugin } @@ -126,17 +126,20 @@ export interface WarnOptions { caseArgs?: Partial | null; contactMethods?: UserNotificationMethod[] | null; retryPromptChannel?: TextChannel | null; + isAutomodAction?: boolean; } export interface KickOptions { caseArgs?: Partial; contactMethods?: UserNotificationMethod[]; + isAutomodAction?: boolean; } export interface BanOptions { caseArgs?: Partial; contactMethods?: UserNotificationMethod[]; deleteMessageDays?: number; + isAutomodAction?: boolean; } export type ModActionType = "note" | "warn" | "mute" | "unmute" | "kick" | "ban" | "unban"; diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts index 29ba54ab..622a5b1a 100644 --- a/backend/src/plugins/Mutes/functions/muteUser.ts +++ b/backend/src/plugins/Mutes/functions/muteUser.ts @@ -247,7 +247,7 @@ export async function muteUser( lock.unlock(); - pluginData.state.events.emit("mute", user.id, reason); + pluginData.state.events.emit("mute", user.id, reason, muteOptions.isAutomodAction); return { case: theCase, diff --git a/backend/src/plugins/Mutes/types.ts b/backend/src/plugins/Mutes/types.ts index 7da2cb0d..ea180e90 100644 --- a/backend/src/plugins/Mutes/types.ts +++ b/backend/src/plugins/Mutes/types.ts @@ -34,7 +34,7 @@ export const ConfigSchema = t.type({ export type TConfigSchema = t.TypeOf; export interface MutesEvents { - mute: (userId: string, reason?: string) => void; + mute: (userId: string, reason?: string, isAutomodAction?: boolean) => void; unmute: (userId: string, reason?: string) => void; } @@ -75,6 +75,7 @@ export type UnmuteResult = { export interface MuteOptions { caseArgs?: Partial; contactMethods?: UserNotificationMethod[]; + isAutomodAction?: boolean; } export const mutesCmd = guildCommand(); From dfc1bf2ba09e317583f27bf5a9e5522b5f12d3b3 Mon Sep 17 00:00:00 2001 From: Shoaib Sajid Date: Thu, 29 Apr 2021 00:08:37 +0500 Subject: [PATCH 11/12] Add !infractions as an alias for !cases (#177) --- backend/src/plugins/ModActions/commands/CasesModCmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 6d84fc77..f9f4667d 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -17,7 +17,7 @@ const opts = { const casesPerPage = 5; export const CasesModCmd = modActionsCmd({ - trigger: ["cases", "modlogs"], + trigger: ["cases", "modlogs", "infractions"], permission: "can_view", description: "Show the most recent 5 cases by the specified -mod", From 20b1c869cd040d1d8bbf7e9a490153db4ca6d861 Mon Sep 17 00:00:00 2001 From: Nils <7890309+DarkView@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:15:16 +0200 Subject: [PATCH 12/12] Add -update/-up argument to automatically update latest/chosen case with !clean (#173) --- .../plugins/ModActions/ModActionsPlugin.ts | 9 +++- .../plugins/ModActions/commands/UpdateCmd.ts | 42 +---------------- .../ModActions/functions/updateCase.ts | 44 +++++++++++++++++ backend/src/plugins/Utility/UtilityPlugin.ts | 3 +- .../src/plugins/Utility/commands/CleanCmd.ts | 47 +++++++++++++++---- 5 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 backend/src/plugins/ModActions/functions/updateCase.ts diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index 672e4630..2d583e06 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -30,7 +30,7 @@ import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; import { ForceUnmuteCmd } from "./commands/ForceunmuteCmd"; import { warnMember } from "./functions/warnMember"; -import { Member } from "eris"; +import { Member, Message } from "eris"; import { kickMember } from "./functions/kickMember"; import { banUserId } from "./functions/banUserId"; import { MassmuteCmd } from "./commands/MassmuteCmd"; @@ -43,6 +43,7 @@ import { EventEmitter } from "events"; import { mapToPublicFn } from "../../pluginUtils"; import { onModActionsEvent } from "./functions/onModActionsEvent"; import { offModActionsEvent } from "./functions/offModActionsEvent"; +import { updateCase } from "./functions/updateCase"; const defaultOptions = { config: { @@ -170,6 +171,12 @@ export const ModActionsPlugin = zeppelinGuildPlugin()("mod }; }, + updateCase(pluginData) { + return (msg: Message, caseNumber: number | null, note: string) => { + updateCase(pluginData, msg, { caseNumber, note }); + }; + }, + on: mapToPublicFn(onModActionsEvent), off: mapToPublicFn(offModActionsEvent), getEventEmitter(pluginData) { diff --git a/backend/src/plugins/ModActions/commands/UpdateCmd.ts b/backend/src/plugins/ModActions/commands/UpdateCmd.ts index 3a048b42..6c8d78ca 100644 --- a/backend/src/plugins/ModActions/commands/UpdateCmd.ts +++ b/backend/src/plugins/ModActions/commands/UpdateCmd.ts @@ -1,11 +1,6 @@ import { modActionsCmd } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { Case } from "../../../data/entities/Case"; -import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; -import { CasesPlugin } from "../../Cases/CasesPlugin"; -import { LogType } from "../../../data/LogType"; -import { CaseTypes } from "../../../data/CaseTypes"; +import { updateCase } from "../functions/updateCase"; export const UpdateCmd = modActionsCmd({ trigger: ["update", "reason"], @@ -24,39 +19,6 @@ export const UpdateCmd = modActionsCmd({ ], async run({ pluginData, message: msg, args }) { - let theCase: Case | undefined; - if (args.caseNumber != null) { - theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber); - } else { - theCase = await pluginData.state.cases.findLatestByModId(msg.author.id); - } - - if (!theCase) { - sendErrorMessage(pluginData, msg.channel, "Case not found"); - return; - } - - if (!args.note && msg.attachments.length === 0) { - sendErrorMessage(pluginData, msg.channel, "Text or attachment required"); - return; - } - - const note = formatReasonWithAttachments(args.note, msg.attachments); - - const casesPlugin = pluginData.getPlugin(CasesPlugin); - await casesPlugin.createCaseNote({ - caseId: theCase.id, - modId: msg.author.id, - body: note, - }); - - pluginData.state.serverLogs.log(LogType.CASE_UPDATE, { - mod: msg.author, - caseNumber: theCase.case_number, - caseType: CaseTypes[theCase.type], - note, - }); - - sendSuccessMessage(pluginData, msg.channel, `Case \`#${theCase.case_number}\` updated`); + await updateCase(pluginData, msg, args); }, }); diff --git a/backend/src/plugins/ModActions/functions/updateCase.ts b/backend/src/plugins/ModActions/functions/updateCase.ts new file mode 100644 index 00000000..12257269 --- /dev/null +++ b/backend/src/plugins/ModActions/functions/updateCase.ts @@ -0,0 +1,44 @@ +import { Message } from "eris"; +import { CaseTypes } from "../../../data/CaseTypes"; +import { Case } from "../../../data/entities/Case"; +import { LogType } from "../../../data/LogType"; +import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; +import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; + +export async function updateCase(pluginData, msg: Message, args) { + let theCase: Case | undefined; + if (args.caseNumber != null) { + theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber); + } else { + theCase = await pluginData.state.cases.findLatestByModId(msg.author.id); + } + + if (!theCase) { + sendErrorMessage(pluginData, msg.channel, "Case not found"); + return; + } + + if (!args.note && msg.attachments.length === 0) { + sendErrorMessage(pluginData, msg.channel, "Text or attachment required"); + return; + } + + const note = formatReasonWithAttachments(args.note, msg.attachments); + + const casesPlugin = pluginData.getPlugin(CasesPlugin); + await casesPlugin.createCaseNote({ + caseId: theCase.id, + modId: msg.author.id, + body: note, + }); + + pluginData.state.serverLogs.log(LogType.CASE_UPDATE, { + mod: msg.author, + caseNumber: theCase.case_number, + caseType: CaseTypes[theCase.type], + note, + }); + + sendSuccessMessage(pluginData, msg.channel, `Case \`#${theCase.case_number}\` updated`); +} diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 90c44c49..e2ba30b0 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -35,6 +35,7 @@ import { SnowflakeInfoCmd } from "./commands/SnowflakeInfoCmd"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { VcdisconnectCmd } from "./commands/VcdisconnectCmd"; +import { ModActionsPlugin } from "../ModActions/ModActionsPlugin"; import { refreshMembersIfNeeded } from "./refreshMembers"; const defaultOptions: PluginOptions = { @@ -106,7 +107,7 @@ export const UtilityPlugin = zeppelinGuildPlugin()("utility", prettyName: "Utility", }, - dependencies: [TimeAndDatePlugin], + dependencies: [TimeAndDatePlugin, ModActionsPlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/Utility/commands/CleanCmd.ts b/backend/src/plugins/Utility/commands/CleanCmd.ts index e75da2f8..8a1a2328 100644 --- a/backend/src/plugins/Utility/commands/CleanCmd.ts +++ b/backend/src/plugins/Utility/commands/CleanCmd.ts @@ -8,6 +8,7 @@ import { GuildPluginData } from "knub"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { LogType } from "../../../data/LogType"; import { allowTimeout } from "../../../RegExpRunner"; +import { ModActionsPlugin } from "../../../plugins/ModActions/ModActionsPlugin"; const MAX_CLEAN_COUNT = 150; const MAX_CLEAN_TIME = 1 * DAYS; @@ -49,23 +50,36 @@ async function cleanMessages( return { archiveUrl }; } +const opts = { + user: ct.userId({ option: true, shortcut: "u" }), + channel: ct.channelId({ option: true, shortcut: "c" }), + bots: ct.switchOption({ shortcut: "b" }), + "delete-pins": ct.switchOption({ shortcut: "p" }), + "has-invites": ct.switchOption({ shortcut: "i" }), + match: ct.regex({ option: true, shortcut: "m" }), + "to-id": ct.anyId({ option: true, shortcut: "id" }), +}; + export const CleanCmd = utilityCmd({ trigger: ["clean", "clear"], description: "Remove a number of recent messages", usage: "!clean 20", permission: "can_clean", - signature: { - count: ct.number(), + signature: [ + { + count: ct.number(), + update: ct.number({ option: true, shortcut: "up" }), - user: ct.userId({ option: true, shortcut: "u" }), - channel: ct.channelId({ option: true, shortcut: "c" }), - bots: ct.switchOption({ shortcut: "b" }), - "delete-pins": ct.switchOption({ shortcut: "p" }), - "has-invites": ct.switchOption({ shortcut: "i" }), - match: ct.regex({ option: true, shortcut: "m" }), - "to-id": ct.anyId({ option: true, shortcut: "id" }), - }, + ...opts, + }, + { + count: ct.number(), + update: ct.switchOption({ shortcut: "up" }), + + ...opts, + }, + ], async run({ message: msg, args, pluginData }) { if (args.count > MAX_CLEAN_COUNT || args.count <= 0) { @@ -155,6 +169,19 @@ export const CleanCmd = utilityCmd({ responseText += ` in <#${targetChannel.id}>\n${cleanResult.archiveUrl}`; } + if (args.update) { + const modActions = pluginData.getPlugin(ModActionsPlugin); + const channelId = targetChannel.id !== msg.channel.id ? targetChannel.id : msg.channel.id; + const updateMessage = `Cleaned ${messagesToClean.length} ${ + messagesToClean.length === 1 ? "message" : "messages" + } in <#${channelId}>: ${cleanResult.archiveUrl}`; + if (typeof args.update === "number") { + modActions.updateCase(msg, args.update, updateMessage); + } else { + modActions.updateCase(msg, null, updateMessage); + } + } + responseMsg = await sendSuccessMessage(pluginData, msg.channel, responseText); } else { responseMsg = await sendErrorMessage(pluginData, msg.channel, `Found no messages to clean!`);