From bb823b627455721006b011273d0285333df384f2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 9 Aug 2020 16:58:36 +0300 Subject: [PATCH] slowmode: fix bot slowmodes not being applied for slowmodes over 6h; add -mode option; add eager permission checks --- .../src/plugins/Slowmode/SlowmodePlugin.ts | 12 +- .../Slowmode/commands/SlowmodeClearCmd.ts | 23 ++- .../Slowmode/commands/SlowmodeDisableCmd.ts | 3 - ...modeGetChannelCmd.ts => SlowmodeGetCmd.ts} | 2 +- .../commands/SlowmodeSetChannelCmd.ts | 93 ----------- .../Slowmode/commands/SlowmodeSetCmd.ts | 154 ++++++++++++++++++ .../plugins/Slowmode/requiredPermissions.ts | 8 + backend/src/plugins/Slowmode/types.ts | 2 +- .../Slowmode/util/actualDisableSlowmodeCmd.ts | 14 ++ .../Slowmode/util/applyBotSlowmodeToUserId.ts | 4 +- .../Slowmode/util/clearExpiredSlowmodes.ts | 2 +- .../plugins/Slowmode/util/onMessageCreate.ts | 20 ++- 12 files changed, 227 insertions(+), 110 deletions(-) rename backend/src/plugins/Slowmode/commands/{SlowmodeGetChannelCmd.ts => SlowmodeGetCmd.ts} (95%) delete mode 100644 backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts create mode 100644 backend/src/plugins/Slowmode/commands/SlowmodeSetCmd.ts create mode 100644 backend/src/plugins/Slowmode/requiredPermissions.ts diff --git a/backend/src/plugins/Slowmode/SlowmodePlugin.ts b/backend/src/plugins/Slowmode/SlowmodePlugin.ts index d27479ea..49405c42 100644 --- a/backend/src/plugins/Slowmode/SlowmodePlugin.ts +++ b/backend/src/plugins/Slowmode/SlowmodePlugin.ts @@ -1,6 +1,6 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; import { PluginOptions } from "knub"; -import { SlowmodePluginType, ConfigSchema } from "./types"; +import { ConfigSchema, SlowmodePluginType } from "./types"; import { GuildSlowmodes } from "src/data/GuildSlowmodes"; import { GuildSavedMessages } from "src/data/GuildSavedMessages"; import { GuildLogs } from "src/data/GuildLogs"; @@ -10,8 +10,9 @@ import { clearExpiredSlowmodes } from "./util/clearExpiredSlowmodes"; import { SlowmodeDisableCmd } from "./commands/SlowmodeDisableCmd"; import { SlowmodeClearCmd } from "./commands/SlowmodeClearCmd"; import { SlowmodeListCmd } from "./commands/SlowmodeListCmd"; -import { SlowmodeGetChannelCmd } from "./commands/SlowmodeGetChannelCmd"; -import { SlowmodeSetChannelCmd } from "./commands/SlowmodeSetChannelCmd"; +import { SlowmodeGetCmd } from "./commands/SlowmodeGetCmd"; +import { SlowmodeSetCmd } from "./commands/SlowmodeSetCmd"; +import { LogsPlugin } from "../Logs/LogsPlugin"; const BOT_SLOWMODE_CLEAR_INTERVAL = 60 * SECONDS; @@ -40,6 +41,7 @@ export const SlowmodePlugin = zeppelinPlugin()("slowmode", { prettyName: "Slowmode", }, + dependencies: [LogsPlugin], configSchema: ConfigSchema, defaultOptions, @@ -48,8 +50,8 @@ export const SlowmodePlugin = zeppelinPlugin()("slowmode", { SlowmodeDisableCmd, SlowmodeClearCmd, SlowmodeListCmd, - SlowmodeGetChannelCmd, - SlowmodeSetChannelCmd, + SlowmodeGetCmd, + SlowmodeSetCmd, ], onLoad(pluginData) { diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts index a5806b41..31eacfb5 100644 --- a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts +++ b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts @@ -2,6 +2,10 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; import { slowmodeCmd } from "../types"; import { clearBotSlowmodeFromUserId } from "../util/clearBotSlowmodeFromUserId"; +import { asSingleLine, disableInlineCode } from "../../../utils"; +import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions"; +import { BOT_SLOWMODE_CLEAR_PERMISSIONS } from "../requiredPermissions"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; export const SlowmodeClearCmd = slowmodeCmd({ trigger: ["slowmode clear", "slowmode c"], @@ -21,14 +25,29 @@ export const SlowmodeClearCmd = slowmodeCmd({ return; } + const me = pluginData.guild.members.get(pluginData.client.user.id); + const missingPermissions = getMissingChannelPermissions(me, args.channel, BOT_SLOWMODE_CLEAR_PERMISSIONS); + if (missingPermissions) { + sendErrorMessage( + pluginData, + msg.channel, + `Unable to clear slowmode. ${missingPermissionError(missingPermissions)}`, + ); + return; + } + try { await clearBotSlowmodeFromUserId(pluginData, args.channel, args.user.id, args.force); } catch (e) { - return sendErrorMessage( + sendErrorMessage( pluginData, msg.channel, - `Failed to clear slowmode from **${args.user.username}#${args.user.discriminator}** in <#${args.channel.id}>`, + asSingleLine(` + Failed to clear slowmode from **${args.user.username}#${args.user.discriminator}** in <#${args.channel.id}>: + \`${disableInlineCode(e.message)}\` + `), ); + return; } sendSuccessMessage( diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts index 20294f2d..1889c559 100644 --- a/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts +++ b/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts @@ -1,8 +1,5 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; import { slowmodeCmd } from "../types"; -import { disableBotSlowmodeForChannel } from "../util/disableBotSlowmodeForChannel"; -import { noop } from "src/utils"; import { actualDisableSlowmodeCmd } from "../util/actualDisableSlowmodeCmd"; export const SlowmodeDisableCmd = slowmodeCmd({ diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeGetChannelCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeGetCmd.ts similarity index 95% rename from backend/src/plugins/Slowmode/commands/SlowmodeGetChannelCmd.ts rename to backend/src/plugins/Slowmode/commands/SlowmodeGetCmd.ts index 589f1373..e9ffe108 100644 --- a/backend/src/plugins/Slowmode/commands/SlowmodeGetChannelCmd.ts +++ b/backend/src/plugins/Slowmode/commands/SlowmodeGetCmd.ts @@ -3,7 +3,7 @@ import { slowmodeCmd } from "../types"; import { TextChannel } from "eris"; import humanizeDuration from "humanize-duration"; -export const SlowmodeGetChannelCmd = slowmodeCmd({ +export const SlowmodeGetCmd = slowmodeCmd({ trigger: "slowmode", permission: "can_manage", source: "guild", diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts deleted file mode 100644 index ab59ce26..00000000 --- a/backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { slowmodeCmd } from "../types"; -import { TextChannel } from "eris"; -import humanizeDuration from "humanize-duration"; -import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; -import { convertDelayStringToMS, HOURS, DAYS } from "src/utils"; -import { disableBotSlowmodeForChannel } from "../util/disableBotSlowmodeForChannel"; -import { actualDisableSlowmodeCmd } from "../util/actualDisableSlowmodeCmd"; - -const NATIVE_SLOWMODE_LIMIT = 6 * HOURS; // 6 hours -const MAX_SLOWMODE = DAYS * 365 * 100; // 100 years - -export const SlowmodeSetChannelCmd = slowmodeCmd({ - trigger: "slowmode", - permission: "can_manage", - source: "guild", - - // prettier-ignore - signature: [ - { - time: ct.string(), - }, - { - channel: ct.textChannel(), - time: ct.string(), - } - ], - - async run({ message: msg, args, pluginData }) { - const channel = args.channel || msg.channel; - - if (channel == null || !(channel instanceof TextChannel)) { - sendErrorMessage(pluginData, msg.channel, "Channel must be a text channel"); - return; - } - - const seconds = Math.ceil(convertDelayStringToMS(args.time, "s") / 1000); - const useNativeSlowmode = - pluginData.config.getForChannel(channel).use_native_slowmode && seconds <= NATIVE_SLOWMODE_LIMIT; - - if (seconds === 0) { - // Workaround until we can call SlowmodeDisableCmd from here - return actualDisableSlowmodeCmd(msg, { channel }, pluginData); - } - - if (seconds > MAX_SLOWMODE) { - sendErrorMessage( - pluginData, - msg.channel, - `Sorry, slowmodes can be at most 100 years long. Maybe 99 would be enough?`, - ); - return; - } - - if (useNativeSlowmode) { - // Native slowmode - - // If there is an existing bot-maintained slowmode, disable that first - const existingBotSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(channel.id); - if (existingBotSlowmode) { - await disableBotSlowmodeForChannel(pluginData, channel); - } - - // Set slowmode - try { - await channel.edit({ - rateLimitPerUser: seconds, - }); - } catch (e) { - return sendErrorMessage(pluginData, msg.channel, "Failed to set native slowmode (check permissions)"); - } - } else { - // Bot-maintained slowmode - - // If there is an existing native slowmode, disable that first - if (channel.rateLimitPerUser) { - await channel.edit({ - rateLimitPerUser: 0, - }); - } - - await pluginData.state.slowmodes.setChannelSlowmode(channel.id, seconds); - } - - const humanizedSlowmodeTime = humanizeDuration(seconds * 1000); - const slowmodeType = useNativeSlowmode ? "native slowmode" : "bot-maintained slowmode"; - sendSuccessMessage( - pluginData, - msg.channel, - `Set ${humanizedSlowmodeTime} slowmode for <#${channel.id}> (${slowmodeType})`, - ); - }, -}); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeSetCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeSetCmd.ts new file mode 100644 index 00000000..d79fcea5 --- /dev/null +++ b/backend/src/plugins/Slowmode/commands/SlowmodeSetCmd.ts @@ -0,0 +1,154 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { slowmodeCmd } from "../types"; +import { TextChannel } from "eris"; +import humanizeDuration from "humanize-duration"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import { asSingleLine, DAYS, disableInlineCode, HOURS, MINUTES } from "src/utils"; +import { disableBotSlowmodeForChannel } from "../util/disableBotSlowmodeForChannel"; +import { actualDisableSlowmodeCmd } from "../util/actualDisableSlowmodeCmd"; +import { getMissingPermissions } from "../../../utils/getMissingPermissions"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; +import { BOT_SLOWMODE_PERMISSIONS, NATIVE_SLOWMODE_PERMISSIONS } from "../requiredPermissions"; + +const MAX_NATIVE_SLOWMODE = 6 * HOURS; // 6 hours +const MAX_BOT_SLOWMODE = DAYS * 365 * 100; // 100 years +const MIN_BOT_SLOWMODE = 15 * MINUTES; + +const validModes = ["bot", "native"]; +type TMode = "bot" | "native"; + +export const SlowmodeSetCmd = slowmodeCmd({ + trigger: "slowmode", + permission: "can_manage", + source: "guild", + + // prettier-ignore + signature: [ + { + time: ct.delay(), + + mode: ct.string({ option: true, shortcut: "m" }), + }, + { + channel: ct.textChannel(), + time: ct.delay(), + + mode: ct.string({ option: true, shortcut: "m" }), + } + ], + + async run({ message: msg, args, pluginData }) { + const channel: TextChannel = args.channel || msg.channel; + + if (args.time === 0) { + // Workaround until we can call SlowmodeDisableCmd from here + return actualDisableSlowmodeCmd(msg, { channel }, pluginData); + } + + const defaultMode: TMode = + pluginData.config.getForChannel(channel).use_native_slowmode && args.time <= MAX_NATIVE_SLOWMODE + ? "native" + : "bot"; + + const mode = (args.mode as TMode) || defaultMode; + if (!validModes.includes(mode)) { + sendErrorMessage(pluginData, msg.channel, "--mode must be 'bot' or 'native'"); + return; + } + + // Validate durations + if (mode === "native" && args.time > MAX_NATIVE_SLOWMODE) { + sendErrorMessage(pluginData, msg.channel, "Native slowmode can only be set to 6h or less"); + return; + } + + if (mode === "bot" && args.time > MAX_BOT_SLOWMODE) { + sendErrorMessage( + pluginData, + msg.channel, + `Sorry, bot managed slowmodes can be at most 100 years long. Maybe 99 would be enough?`, + ); + return; + } + + if (mode === "bot" && args.time < MIN_BOT_SLOWMODE) { + sendErrorMessage( + pluginData, + msg.channel, + asSingleLine(` + Bot managed slowmode must be 15min or more. + Use \`--mode native\` to use native slowmodes for short slowmodes instead. + `), + ); + return; + } + + // Verify permissions + const channelPermissions = channel.permissionsOf(pluginData.client.user.id); + + if (mode === "native") { + const missingPermissions = getMissingPermissions(channelPermissions, NATIVE_SLOWMODE_PERMISSIONS); + if (missingPermissions) { + sendErrorMessage( + pluginData, + msg.channel, + `Unable to set native slowmode. ${missingPermissionError(missingPermissions)}`, + ); + return; + } + } + + if (mode === "bot") { + const missingPermissions = getMissingPermissions(channelPermissions, BOT_SLOWMODE_PERMISSIONS); + if (missingPermissions) { + sendErrorMessage( + pluginData, + msg.channel, + `Unable to set bot managed slowmode. ${missingPermissionError(missingPermissions)}`, + ); + return; + } + } + + // Apply the slowmode! + const rateLimitSeconds = Math.ceil(args.time / 1000); + + if (mode === "native") { + // If there is an existing bot-maintained slowmode, disable that first + const existingBotSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(channel.id); + if (existingBotSlowmode) { + await disableBotSlowmodeForChannel(pluginData, channel); + } + + // Set native slowmode + try { + await channel.edit({ + rateLimitPerUser: rateLimitSeconds, + }); + } catch (e) { + return sendErrorMessage( + pluginData, + msg.channel, + `Failed to set native slowmode: ${disableInlineCode(e.message)}`, + ); + } + } else { + // If there is an existing native slowmode, disable that first + if (channel.rateLimitPerUser) { + await channel.edit({ + rateLimitPerUser: 0, + }); + } + + await pluginData.state.slowmodes.setChannelSlowmode(channel.id, rateLimitSeconds); + } + + const humanizedSlowmodeTime = humanizeDuration(args.time); + const slowmodeType = mode === "native" ? "native slowmode" : "bot-maintained slowmode"; + sendSuccessMessage( + pluginData, + msg.channel, + `Set ${humanizedSlowmodeTime} slowmode for <#${channel.id}> (${slowmodeType})`, + ); + }, +}); diff --git a/backend/src/plugins/Slowmode/requiredPermissions.ts b/backend/src/plugins/Slowmode/requiredPermissions.ts new file mode 100644 index 00000000..7b1b52f5 --- /dev/null +++ b/backend/src/plugins/Slowmode/requiredPermissions.ts @@ -0,0 +1,8 @@ +import { Constants } from "eris"; + +const p = Constants.Permissions; + +export const NATIVE_SLOWMODE_PERMISSIONS = p.readMessages | p.manageChannels; +export const BOT_SLOWMODE_PERMISSIONS = p.readMessages | p.manageRoles | p.manageMessages; +export const BOT_SLOWMODE_CLEAR_PERMISSIONS = p.readMessages | p.manageRoles; +export const BOT_SLOWMODE_DISABLE_PERMISSIONS = p.readMessages | p.manageRoles; diff --git a/backend/src/plugins/Slowmode/types.ts b/backend/src/plugins/Slowmode/types.ts index 0f956644..9f37e67d 100644 --- a/backend/src/plugins/Slowmode/types.ts +++ b/backend/src/plugins/Slowmode/types.ts @@ -1,5 +1,5 @@ import * as t from "io-ts"; -import { BasePluginType, eventListener, command } from "knub"; +import { BasePluginType, command, eventListener } from "knub"; import { GuildSlowmodes } from "src/data/GuildSlowmodes"; import { GuildSavedMessages } from "src/data/GuildSavedMessages"; import { GuildLogs } from "src/data/GuildLogs"; diff --git a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts index 948c474a..607e9c0d 100644 --- a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts +++ b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts @@ -2,6 +2,9 @@ import { Message } from "eris"; import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; import { disableBotSlowmodeForChannel } from "./disableBotSlowmodeForChannel"; import { noop } from "src/utils"; +import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions"; +import { BOT_SLOWMODE_DISABLE_PERMISSIONS } from "../requiredPermissions"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; export async function actualDisableSlowmodeCmd(msg: Message, args, pluginData) { const botSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(args.channel.id); @@ -12,6 +15,17 @@ export async function actualDisableSlowmodeCmd(msg: Message, args, pluginData) { return; } + const me = pluginData.guild.members.get(pluginData.client.user.id); + const missingPermissions = getMissingChannelPermissions(me, args.channel, BOT_SLOWMODE_DISABLE_PERMISSIONS); + if (missingPermissions) { + sendErrorMessage( + pluginData, + msg.channel, + `Unable to disable slowmode. ${missingPermissionError(missingPermissions)}`, + ); + return; + } + const initMsg = await msg.channel.createMessage("Disabling slowmode..."); // Disable bot-maintained slowmode diff --git a/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts b/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts index 0b22a738..300998bd 100644 --- a/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts +++ b/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts @@ -1,7 +1,7 @@ import { SlowmodePluginType } from "../types"; import { PluginData } from "knub"; -import { GuildChannel, TextChannel, Constants } from "eris"; -import { UnknownUser, isDiscordRESTError, stripObjectToScalars } from "src/utils"; +import { Constants, GuildChannel, TextChannel } from "eris"; +import { isDiscordRESTError, stripObjectToScalars, UnknownUser } from "src/utils"; import { LogType } from "src/data/LogType"; import { logger } from "src/logger"; diff --git a/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts b/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts index 1f0889a9..01cb95c1 100644 --- a/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts +++ b/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts @@ -3,7 +3,7 @@ import { SlowmodePluginType } from "../types"; import { LogType } from "src/data/LogType"; import { logger } from "src/logger"; import { GuildChannel, TextChannel } from "eris"; -import { UnknownUser, stripObjectToScalars } from "src/utils"; +import { stripObjectToScalars, UnknownUser } from "src/utils"; import { clearBotSlowmodeFromUserId } from "./clearBotSlowmodeFromUserId"; export async function clearExpiredSlowmodes(pluginData: PluginData) { diff --git a/backend/src/plugins/Slowmode/util/onMessageCreate.ts b/backend/src/plugins/Slowmode/util/onMessageCreate.ts index 13bebcd3..81e9799f 100644 --- a/backend/src/plugins/Slowmode/util/onMessageCreate.ts +++ b/backend/src/plugins/Slowmode/util/onMessageCreate.ts @@ -1,15 +1,20 @@ import { SavedMessage } from "src/data/entities/SavedMessage"; -import { GuildChannel, TextChannel } from "eris"; +import { TextChannel } from "eris"; import { PluginData } from "knub"; import { SlowmodePluginType } from "../types"; import { resolveMember } from "src/utils"; import { applyBotSlowmodeToUserId } from "./applyBotSlowmodeToUserId"; import { hasPermission } from "src/pluginUtils"; +import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions"; +import { BOT_SLOWMODE_PERMISSIONS } from "../requiredPermissions"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { LogType } from "../../../data/LogType"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; export async function onMessageCreate(pluginData: PluginData, msg: SavedMessage) { if (msg.is_bot) return; - const channel = pluginData.guild.channels.get(msg.channel_id) as GuildChannel & TextChannel; + const channel = pluginData.guild.channels.get(msg.channel_id) as TextChannel; if (!channel) return; // Don't apply slowmode if the lock was interrupted earlier (e.g. the message was caught by word filters) @@ -25,6 +30,17 @@ export async function onMessageCreate(pluginData: PluginData const isAffected = hasPermission(pluginData, "is_affected", { channelId: channel.id, userId: msg.user_id, member }); if (!isAffected) return thisMsgLock.unlock(); + // Make sure we have the appropriate permissions to manage this slowmode + const me = pluginData.guild.members.get(pluginData.client.user.id); + const missingPermissions = getMissingChannelPermissions(me, channel, BOT_SLOWMODE_PERMISSIONS); + if (missingPermissions) { + const logs = pluginData.getPlugin(LogsPlugin); + logs.log(LogType.BOT_ALERT, { + body: `Unable to manage bot slowmode in <#${channel.id}>. ${missingPermissionError(missingPermissions)}`, + }); + return; + } + // Delete any extra messages sent after a slowmode was already applied const userHasSlowmode = await pluginData.state.slowmodes.userHasSlowmode(channel.id, msg.user_id); if (userHasSlowmode) {