diff --git a/backend/src/plugins/Slowmode/SlowmodePlugin.ts b/backend/src/plugins/Slowmode/SlowmodePlugin.ts new file mode 100644 index 00000000..91154385 --- /dev/null +++ b/backend/src/plugins/Slowmode/SlowmodePlugin.ts @@ -0,0 +1,65 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { PluginOptions } from "knub"; +import { SlowmodePluginType, ConfigSchema } from "./types"; +import { GuildSlowmodes } from "src/data/GuildSlowmodes"; +import { GuildSavedMessages } from "src/data/GuildSavedMessages"; +import { GuildLogs } from "src/data/GuildLogs"; +import { SECONDS } from "src/utils"; +import { onMessageCreate } from "./util/onMessageCreate"; +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"; + +const BOT_SLOWMODE_CLEAR_INTERVAL = 60 * SECONDS; + +const defaultOptions: PluginOptions = { + config: { + use_native_slowmode: true, + + can_manage: false, + is_affected: true, + }, + + overrides: [ + { + level: ">=50", + config: { + can_manage: true, + is_affected: false, + }, + }, + ], +}; + +export const SlowmodePlugin = zeppelinPlugin()("slowmode", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + SlowmodeDisableCmd, + SlowmodeClearCmd, + SlowmodeListCmd, + SlowmodeGetChannelCmd, + SlowmodeSetChannelCmd, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.slowmodes = GuildSlowmodes.getGuildInstance(guild.id); + state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id); + state.logs = new GuildLogs(guild.id); + state.clearInterval = setInterval(() => clearExpiredSlowmodes(pluginData), BOT_SLOWMODE_CLEAR_INTERVAL); + + state.onMessageCreateFn = msg => onMessageCreate(pluginData, msg); + state.savedMessages.events.on("create", state.onMessageCreateFn); + }, + + onUnload(pluginData) { + pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn); + }, +}); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts new file mode 100644 index 00000000..a5806b41 --- /dev/null +++ b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts @@ -0,0 +1,40 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import { slowmodeCmd } from "../types"; +import { clearBotSlowmodeFromUserId } from "../util/clearBotSlowmodeFromUserId"; + +export const SlowmodeClearCmd = slowmodeCmd({ + trigger: ["slowmode clear", "slowmode c"], + permission: "can_manage", + + signature: { + channel: ct.textChannel(), + user: ct.resolvedUserLoose(), + + force: ct.bool({ option: true, isSwitch: true }), + }, + + async run({ message: msg, args, pluginData }) { + const channelSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(args.channel.id); + if (!channelSlowmode) { + sendErrorMessage(pluginData, msg.channel, "Channel doesn't have slowmode!"); + return; + } + + try { + await clearBotSlowmodeFromUserId(pluginData, args.channel, args.user.id, args.force); + } catch (e) { + return sendErrorMessage( + pluginData, + msg.channel, + `Failed to clear slowmode from **${args.user.username}#${args.user.discriminator}** in <#${args.channel.id}>`, + ); + } + + sendSuccessMessage( + pluginData, + msg.channel, + `Slowmode cleared from **${args.user.username}#${args.user.discriminator}** in <#${args.channel.id}>`, + ); + }, +}); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts new file mode 100644 index 00000000..20294f2d --- /dev/null +++ b/backend/src/plugins/Slowmode/commands/SlowmodeDisableCmd.ts @@ -0,0 +1,20 @@ +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({ + trigger: ["slowmode disable", "slowmode d"], + permission: "can_manage", + + signature: { + channel: ct.textChannel(), + }, + + async run({ message: msg, args, pluginData }) { + // Workaround until you can call this cmd from SlowmodeSetChannelCmd + actualDisableSlowmodeCmd(msg, args, pluginData); + }, +}); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeGetChannelCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeGetChannelCmd.ts new file mode 100644 index 00000000..589f1373 --- /dev/null +++ b/backend/src/plugins/Slowmode/commands/SlowmodeGetChannelCmd.ts @@ -0,0 +1,37 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { slowmodeCmd } from "../types"; +import { TextChannel } from "eris"; +import humanizeDuration from "humanize-duration"; + +export const SlowmodeGetChannelCmd = slowmodeCmd({ + trigger: "slowmode", + permission: "can_manage", + source: "guild", + + signature: { + channel: ct.textChannel({ option: true }), + }, + + async run({ message: msg, args, pluginData }) { + const channel = args.channel || (msg.channel as TextChannel); + + let currentSlowmode = channel.rateLimitPerUser; + let isNative = true; + + if (!currentSlowmode) { + const botSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(channel.id); + if (botSlowmode) { + currentSlowmode = botSlowmode.slowmode_seconds; + isNative = false; + } + } + + if (currentSlowmode) { + const humanized = humanizeDuration(channel.rateLimitPerUser * 1000); + const slowmodeType = isNative ? "native" : "bot-maintained"; + msg.channel.createMessage(`The current slowmode of <#${channel.id}> is **${humanized}** (${slowmodeType})`); + } else { + msg.channel.createMessage("Channel is not on slowmode"); + } + }, +}); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeListCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeListCmd.ts new file mode 100644 index 00000000..422cb503 --- /dev/null +++ b/backend/src/plugins/Slowmode/commands/SlowmodeListCmd.ts @@ -0,0 +1,46 @@ +import { slowmodeCmd } from "../types"; +import { GuildChannel, TextChannel } from "eris"; +import { createChunkedMessage } from "knub/dist/helpers"; +import { errorMessage } from "src/utils"; +import humanizeDuration from "humanize-duration"; + +export const SlowmodeListCmd = slowmodeCmd({ + trigger: ["slowmode list", "slowmode l", "slowmodes"], + permission: "can_manage", + + async run({ message: msg, pluginData }) { + const channels = pluginData.guild.channels; + const slowmodes: Array<{ channel: GuildChannel; seconds: number; native: boolean }> = []; + + for (const channel of channels.values()) { + if (!(channel instanceof TextChannel)) continue; + + // Bot slowmode + const botSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(channel.id); + if (botSlowmode) { + slowmodes.push({ channel, seconds: botSlowmode.slowmode_seconds, native: false }); + continue; + } + + // Native slowmode + if (channel.rateLimitPerUser) { + slowmodes.push({ channel, seconds: channel.rateLimitPerUser, native: true }); + continue; + } + } + + if (slowmodes.length) { + const lines = slowmodes.map(slowmode => { + const humanized = humanizeDuration(slowmode.seconds * 1000); + + const type = slowmode.native ? "native slowmode" : "bot slowmode"; + + return `<#${slowmode.channel.id}> **${humanized}** ${type}`; + }); + + createChunkedMessage(msg.channel, lines.join("\n")); + } else { + msg.channel.createMessage(errorMessage("No active slowmodes!")); + } + }, +}); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts new file mode 100644 index 00000000..ab59ce26 --- /dev/null +++ b/backend/src/plugins/Slowmode/commands/SlowmodeSetChannelCmd.ts @@ -0,0 +1,93 @@ +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/types.ts b/backend/src/plugins/Slowmode/types.ts new file mode 100644 index 00000000..0f956644 --- /dev/null +++ b/backend/src/plugins/Slowmode/types.ts @@ -0,0 +1,28 @@ +import * as t from "io-ts"; +import { BasePluginType, eventListener, command } from "knub"; +import { GuildSlowmodes } from "src/data/GuildSlowmodes"; +import { GuildSavedMessages } from "src/data/GuildSavedMessages"; +import { GuildLogs } from "src/data/GuildLogs"; + +export const ConfigSchema = t.type({ + use_native_slowmode: t.boolean, + + can_manage: t.boolean, + is_affected: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface SlowmodePluginType extends BasePluginType { + config: TConfigSchema; + state: { + slowmodes: GuildSlowmodes; + savedMessages: GuildSavedMessages; + logs: GuildLogs; + clearInterval: NodeJS.Timeout; + + onMessageCreateFn; + }; +} + +export const slowmodeCmd = command(); +export const slowmodeEvt = eventListener(); diff --git a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts new file mode 100644 index 00000000..948c474a --- /dev/null +++ b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts @@ -0,0 +1,39 @@ +import { Message } from "eris"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import { disableBotSlowmodeForChannel } from "./disableBotSlowmodeForChannel"; +import { noop } from "src/utils"; + +export async function actualDisableSlowmodeCmd(msg: Message, args, pluginData) { + const botSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(args.channel.id); + const hasNativeSlowmode = args.channel.rateLimitPerUser; + + if (!botSlowmode && hasNativeSlowmode === 0) { + sendErrorMessage(pluginData, msg.channel, "Channel is not on slowmode!"); + return; + } + + const initMsg = await msg.channel.createMessage("Disabling slowmode..."); + + // Disable bot-maintained slowmode + let failedUsers = []; + if (botSlowmode) { + const result = await disableBotSlowmodeForChannel(pluginData, args.channel); + failedUsers = result.failedUsers; + } + + // Disable native slowmode + if (hasNativeSlowmode) { + await args.channel.edit({ rateLimitPerUser: 0 }); + } + + if (failedUsers.length) { + sendSuccessMessage( + pluginData, + msg.channel, + `Slowmode disabled! Failed to clear slowmode from the following users:\n\n<@!${failedUsers.join(">\n<@!")}>`, + ); + } else { + sendSuccessMessage(pluginData, msg.channel, "Slowmode disabled!"); + initMsg.delete().catch(noop); + } +} diff --git a/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts b/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts new file mode 100644 index 00000000..0b22a738 --- /dev/null +++ b/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts @@ -0,0 +1,43 @@ +import { SlowmodePluginType } from "../types"; +import { PluginData } from "knub"; +import { GuildChannel, TextChannel, Constants } from "eris"; +import { UnknownUser, isDiscordRESTError, stripObjectToScalars } from "src/utils"; +import { LogType } from "src/data/LogType"; +import { logger } from "src/logger"; + +export async function applyBotSlowmodeToUserId( + pluginData: PluginData, + channel: GuildChannel & TextChannel, + userId: string, +) { + // Deny sendMessage permission from the user. If there are existing permission overwrites, take those into account. + const existingOverride = channel.permissionOverwrites.get(userId); + const newDeniedPermissions = (existingOverride ? existingOverride.deny : 0) | Constants.Permissions.sendMessages; + const newAllowedPermissions = (existingOverride ? existingOverride.allow : 0) & ~Constants.Permissions.sendMessages; + + try { + await channel.editPermission(userId, newAllowedPermissions, newDeniedPermissions, "member"); + } catch (e) { + const user = pluginData.client.users.get(userId) || new UnknownUser({ id: userId }); + + if (isDiscordRESTError(e) && e.code === 50013) { + logger.warn( + `Missing permissions to apply bot slowmode to user ${userId} on channel ${channel.name} (${channel.id}) on server ${pluginData.guild.name} (${pluginData.guild.id})`, + ); + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Missing permissions to apply bot slowmode to {userMention(user)} in {channelMention(channel)}`, + user: stripObjectToScalars(user), + channel: stripObjectToScalars(channel), + }); + } else { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Failed to apply bot slowmode to {userMention(user)} in {channelMention(channel)}`, + user: stripObjectToScalars(user), + channel: stripObjectToScalars(channel), + }); + throw e; + } + } + + await pluginData.state.slowmodes.addSlowmodeUser(channel.id, userId); +} diff --git a/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts b/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts new file mode 100644 index 00000000..dcde2322 --- /dev/null +++ b/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts @@ -0,0 +1,24 @@ +import { PluginData } from "knub"; +import { SlowmodePluginType } from "../types"; +import { GuildChannel, TextChannel } from "eris"; + +export async function clearBotSlowmodeFromUserId( + pluginData: PluginData, + channel: GuildChannel & TextChannel, + userId: string, + force = false, +) { + try { + // Remove permission overrides from the channel for this user + // Previously we diffed the overrides so we could clear the "send messages" override without touching other + // overrides. Unfortunately, it seems that was a bit buggy - we didn't always receive the event for the changed + // overrides and then we also couldn't diff against them. For consistency's sake, we just delete the override now. + await channel.deletePermission(userId); + } catch (e) { + if (!force) { + throw e; + } + } + + await pluginData.state.slowmodes.clearSlowmodeUser(channel.id, userId); +} diff --git a/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts b/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts new file mode 100644 index 00000000..1f0889a9 --- /dev/null +++ b/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts @@ -0,0 +1,31 @@ +import { PluginData } from "knub"; +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 { clearBotSlowmodeFromUserId } from "./clearBotSlowmodeFromUserId"; + +export async function clearExpiredSlowmodes(pluginData: PluginData) { + const expiredSlowmodeUsers = await pluginData.state.slowmodes.getExpiredSlowmodeUsers(); + for (const user of expiredSlowmodeUsers) { + const channel = pluginData.guild.channels.get(user.channel_id); + if (!channel) { + await pluginData.state.slowmodes.clearSlowmodeUser(user.channel_id, user.user_id); + continue; + } + + try { + await clearBotSlowmodeFromUserId(pluginData, channel as GuildChannel & TextChannel, user.user_id); + } catch (e) { + logger.error(e); + + const realUser = pluginData.client.users.get(user.user_id) || new UnknownUser({ id: user.user_id }); + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Failed to clear slowmode permissions from {userMention(user)} in {channelMention(channel)}`, + user: stripObjectToScalars(realUser), + channel: stripObjectToScalars(channel), + }); + } + } +} diff --git a/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts b/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts new file mode 100644 index 00000000..f505a9c1 --- /dev/null +++ b/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts @@ -0,0 +1,28 @@ +import { GuildChannel, TextChannel } from "eris"; +import { PluginData } from "knub"; +import { SlowmodePluginType } from "../types"; +import { clearBotSlowmodeFromUserId } from "./clearBotSlowmodeFromUserId"; + +export async function disableBotSlowmodeForChannel( + pluginData: PluginData, + channel: GuildChannel & TextChannel, +) { + // Disable channel slowmode + await pluginData.state.slowmodes.deleteChannelSlowmode(channel.id); + + // Remove currently applied slowmodes + const users = await pluginData.state.slowmodes.getChannelSlowmodeUsers(channel.id); + const failedUsers = []; + + for (const slowmodeUser of users) { + try { + await clearBotSlowmodeFromUserId(pluginData, channel, slowmodeUser.user_id); + } catch (e) { + // Removing the slowmode failed. Record this so the permissions can be changed manually, and remove the database entry. + failedUsers.push(slowmodeUser.user_id); + await pluginData.state.slowmodes.clearSlowmodeUser(channel.id, slowmodeUser.user_id); + } + } + + return { failedUsers }; +} diff --git a/backend/src/plugins/Slowmode/util/onMessageCreate.ts b/backend/src/plugins/Slowmode/util/onMessageCreate.ts new file mode 100644 index 00000000..13bebcd3 --- /dev/null +++ b/backend/src/plugins/Slowmode/util/onMessageCreate.ts @@ -0,0 +1,42 @@ +import { SavedMessage } from "src/data/entities/SavedMessage"; +import { GuildChannel, 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"; + +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; + if (!channel) return; + + // Don't apply slowmode if the lock was interrupted earlier (e.g. the message was caught by word filters) + const thisMsgLock = await pluginData.locks.acquire(`message-${msg.id}`); + if (thisMsgLock.interrupted) return; + + // Check if this channel even *has* a bot-maintained slowmode + const channelSlowmode = await pluginData.state.slowmodes.getChannelSlowmode(channel.id); + if (!channelSlowmode) return thisMsgLock.unlock(); + + // Make sure this user is affected by the slowmode + const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id); + const isAffected = hasPermission(pluginData, "is_affected", { channelId: channel.id, userId: msg.user_id, member }); + if (!isAffected) return thisMsgLock.unlock(); + + // 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) { + const message = await channel.getMessage(msg.id); + if (message) { + message.delete(); + return thisMsgLock.interrupt(); + } + + return thisMsgLock.unlock(); + } + + await applyBotSlowmodeToUserId(pluginData, channel, msg.user_id); + thisMsgLock.unlock(); +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 32825725..f5b1acb3 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -13,6 +13,7 @@ import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigRelo import { CasesPlugin } from "./Cases/CasesPlugin"; import { MutesPlugin } from "./Mutes/MutesPlugin"; import { TagsPlugin } from "./Tags/TagsPlugin"; +import { SlowmodePlugin } from "./Slowmode/SlowmodePlugin"; // prettier-ignore export const guildPlugins: Array> = [ @@ -23,6 +24,7 @@ export const guildPlugins: Array> = [ MessageSaverPlugin, NameHistoryPlugin, RemindersPlugin, + SlowmodePlugin, TagsPlugin, UsernameSaverPlugin, UtilityPlugin,