From c1d91b28645b1f8a9a9047ba2a1c7c7c5ca560d8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 6 Jul 2020 02:56:54 +0300 Subject: [PATCH] Convert !clean --- backend/src/pluginUtils.ts | 4 +- backend/src/plugins/Utility/UtilityPlugin.ts | 2 + .../src/plugins/Utility/commands/CleanCmd.ts | 142 ++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 backend/src/plugins/Utility/commands/CleanCmd.ts diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 7998787f..56422b2a 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -54,12 +54,12 @@ export function pluginConfigPreprocessor( export function sendSuccessMessage(pluginData: PluginData, channel, body) { const emoji = pluginData.guildConfig.success_emoji || undefined; - channel.createMessage(successMessage(body, emoji)); + return channel.createMessage(successMessage(body, emoji)); } export function sendErrorMessage(pluginData: PluginData, channel, body) { const emoji = pluginData.guildConfig.error_emoji || undefined; - channel.createMessage(errorMessage(body, emoji)); + return channel.createMessage(errorMessage(body, emoji)); } export function getBaseUrl(pluginData: PluginData) { diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 2da3f28f..63b207f4 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -25,6 +25,7 @@ import { sendSuccessMessage } from "../../pluginUtils"; import { ReloadGuildCmd } from "./commands/ReloadGuildCmd"; import { JumboCmd } from "./commands/JumboCmd"; import { AvatarCmd } from "./commands/AvatarCmd"; +import { CleanCmd } from "./commands/CleanCmd"; const defaultOptions: PluginOptions = { config: { @@ -99,6 +100,7 @@ export const UtilityPlugin = zeppelinPlugin()("utility", { ReloadGuildCmd, JumboCmd, AvatarCmd, + CleanCmd, ], onLoad(pluginData) { diff --git a/backend/src/plugins/Utility/commands/CleanCmd.ts b/backend/src/plugins/Utility/commands/CleanCmd.ts new file mode 100644 index 00000000..89605cd8 --- /dev/null +++ b/backend/src/plugins/Utility/commands/CleanCmd.ts @@ -0,0 +1,142 @@ +import { utilityCmd, UtilityPluginType } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { DAYS, getInviteCodesInString, noop, SECONDS, stripObjectToScalars } from "../../../utils"; +import { getBaseUrl, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { Message, TextChannel, User } from "eris"; +import moment from "moment-timezone"; +import { PluginData } from "knub"; +import { SavedMessage } from "../../../data/entities/SavedMessage"; +import { LogType } from "../../../data/LogType"; + +const MAX_CLEAN_COUNT = 150; +const MAX_CLEAN_TIME = 1 * DAYS; +const CLEAN_COMMAND_DELETE_DELAY = 5 * SECONDS; + +async function cleanMessages( + pluginData: PluginData, + channel: TextChannel, + savedMessages: SavedMessage[], + mod: User, +) { + pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id); + pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id); + + // Delete & archive in ID order + savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1)); + const idsToDelete = savedMessages.map(m => m.id); + + // Make sure the deletions aren't double logged + idsToDelete.forEach(id => pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id)); + pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]); + + // Actually delete the messages + await pluginData.client.deleteMessages(channel.id, idsToDelete); + await pluginData.state.savedMessages.markBulkAsDeleted(idsToDelete); + + // Create an archive + const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild); + const baseUrl = getBaseUrl(pluginData); + const archiveUrl = pluginData.state.archives.getUrl(baseUrl, archiveId); + + pluginData.state.logs.log(LogType.CLEAN, { + mod: stripObjectToScalars(mod), + channel: stripObjectToScalars(channel), + count: savedMessages.length, + archiveUrl, + }); + + return { archiveUrl }; +} + +export const CleanCmd = utilityCmd({ + trigger: "clean", + description: "Remove a number of recent messages", + usage: "!clean 20", + permission: "can_clean", + + signature: { + count: ct.number(), + + user: ct.userId({ option: true, shortcut: "u" }), + channel: ct.channelId({ option: true, shortcut: "c" }), + bots: ct.switchOption({ shortcut: "b" }), + "has-invites": ct.switchOption({ shortcut: "i" }), + }, + + async run({ message: msg, args, pluginData }) { + if (args.count > MAX_CLEAN_COUNT || args.count <= 0) { + sendErrorMessage(pluginData, msg.channel, `Clean count must be between 1 and ${MAX_CLEAN_COUNT}`); + return; + } + + const targetChannel = args.channel ? pluginData.guild.channels.get(args.channel) : msg.channel; + if (!targetChannel || !(targetChannel instanceof TextChannel)) { + sendErrorMessage(pluginData, msg.channel, `Invalid channel specified`); + return; + } + + if (targetChannel.id !== msg.channel.id) { + const configForTargetChannel = pluginData.config.getMatchingConfig({ + userId: msg.author.id, + channelId: targetChannel.id, + }); + if (configForTargetChannel.can_clean !== true) { + sendErrorMessage(pluginData, msg.channel, `Missing permissions to use clean on that channel`); + return; + } + } + + const messagesToClean = []; + let beforeId = msg.id; + const timeCutoff = msg.timestamp - MAX_CLEAN_TIME; + + while (messagesToClean.length < args.count) { + const potentialMessagesToClean = await pluginData.state.savedMessages.getLatestByChannelBeforeId( + targetChannel.id, + beforeId, + args.count, + ); + if (potentialMessagesToClean.length === 0) break; + + const filtered = potentialMessagesToClean.filter(message => { + if (args.user && message.user_id !== args.user) return false; + if (args.bots && !message.is_bot) return false; + if (args["has-invites"] && getInviteCodesInString(message.data.content || "").length === 0) return false; + if (moment.utc(message.posted_at).valueOf() < timeCutoff) return false; + return true; + }); + const remaining = args.count - messagesToClean.length; + const withoutOverflow = filtered.slice(0, remaining); + messagesToClean.push(...withoutOverflow); + + beforeId = potentialMessagesToClean[potentialMessagesToClean.length - 1].id; + + if (moment.utc(potentialMessagesToClean[potentialMessagesToClean.length - 1].posted_at).valueOf() < timeCutoff) { + break; + } + } + + let responseMsg: Message; + if (messagesToClean.length > 0) { + const cleanResult = await cleanMessages(pluginData, targetChannel, messagesToClean, msg.author); + + let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`; + if (targetChannel.id !== msg.channel.id) { + responseText += ` in <#${targetChannel.id}>\n${cleanResult.archiveUrl}`; + } + + responseMsg = await sendSuccessMessage(pluginData, msg.channel, responseText); + } else { + responseMsg = await sendErrorMessage(pluginData, msg.channel, `Found no messages to clean!`); + } + + if (targetChannel.id === msg.channel.id) { + // Delete the !clean command and the bot response if a different channel wasn't specified + // (so as not to spam the cleaned channel with the command itself) + setTimeout(() => { + msg.delete().catch(noop); + responseMsg.delete().catch(noop); + }, CLEAN_COMMAND_DELETE_DELAY); + } + }, +});