diff --git a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts new file mode 100644 index 00000000..ce55d90f --- /dev/null +++ b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts @@ -0,0 +1,48 @@ +import { PluginOptions } from "knub"; +import { AutoDeletePluginType, ConfigSchema } from "./types"; +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { GuildSavedMessages } from "src/data/GuildSavedMessages"; +import { GuildLogs } from "src/data/GuildLogs"; +import { onMessageCreate } from "./util/onMessageCreate"; +import { onMessageDelete } from "./util/onMessageDelete"; +import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk"; + +const defaultOptions: PluginOptions = { + config: { + enabled: false, + delay: "5s", + }, +}; + +export const AutoDeletePlugin = zeppelinPlugin()("auto_delete", { + configSchema: ConfigSchema, + defaultOptions, + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.guildSavedMessages = GuildSavedMessages.getGuildInstance(guild.id); + state.guildLogs = new GuildLogs(guild.id); + + state.deletionQueue = []; + state.nextDeletion = null; + state.nextDeletionTimeout = null; + + state.maxDelayWarningSent = false; + + state.onMessageCreateFn = msg => onMessageCreate(pluginData, msg); + state.guildSavedMessages.events.on("create", state.onMessageCreateFn); + + state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg); + state.guildSavedMessages.events.on("delete", state.onMessageDeleteFn); + + state.onMessageDeleteBulkFn = msgs => onMessageDeleteBulk(pluginData, msgs); + state.guildSavedMessages.events.on("deleteBulk", state.onMessageDeleteBulkFn); + }, + + onUnload(pluginData) { + pluginData.state.guildSavedMessages.events.off("create", pluginData.state.onMessageCreateFn); + pluginData.state.guildSavedMessages.events.off("delete", pluginData.state.onMessageDeleteFn); + pluginData.state.guildSavedMessages.events.off("deleteBulk", pluginData.state.onMessageDeleteBulkFn); + }, +}); diff --git a/backend/src/plugins/AutoDelete/types.ts b/backend/src/plugins/AutoDelete/types.ts new file mode 100644 index 00000000..1eb9342d --- /dev/null +++ b/backend/src/plugins/AutoDelete/types.ts @@ -0,0 +1,37 @@ +import * as t from "io-ts"; +import { BasePluginType } from "knub"; +import { tDelayString, MINUTES } from "src/utils"; +import { GuildLogs } from "src/data/GuildLogs"; +import { GuildSavedMessages } from "src/data/GuildSavedMessages"; +import { SavedMessage } from "src/data/entities/SavedMessage"; + +export const MAX_DELAY = 5 * MINUTES; + +export interface IDeletionQueueItem { + deleteAt: number; + message: SavedMessage; +} + +export const ConfigSchema = t.type({ + enabled: t.boolean, + delay: tDelayString, +}); +export type TConfigSchema = t.TypeOf; + +export interface AutoDeletePluginType extends BasePluginType { + config: TConfigSchema; + state: { + guildSavedMessages: GuildSavedMessages; + guildLogs: GuildLogs; + + deletionQueue: IDeletionQueueItem[]; + nextDeletion: number; + nextDeletionTimeout; + + maxDelayWarningSent: boolean; + + onMessageCreateFn; + onMessageDeleteFn; + onMessageDeleteBulkFn; + }; +} diff --git a/backend/src/plugins/AutoDelete/util/addMessageToDeletionQueue.ts b/backend/src/plugins/AutoDelete/util/addMessageToDeletionQueue.ts new file mode 100644 index 00000000..b727a3bc --- /dev/null +++ b/backend/src/plugins/AutoDelete/util/addMessageToDeletionQueue.ts @@ -0,0 +1,17 @@ +import { PluginData } from "knub"; +import { AutoDeletePluginType } from "../types"; +import { SavedMessage } from "src/data/entities/SavedMessage"; +import { scheduleNextDeletion } from "./scheduleNextDeletion"; +import { sorter } from "src/utils"; + +export function addMessageToDeletionQueue( + pluginData: PluginData, + msg: SavedMessage, + delay: number, +) { + const deleteAt = Date.now() + delay; + pluginData.state.deletionQueue.push({ deleteAt, message: msg }); + pluginData.state.deletionQueue.sort(sorter("deleteAt")); + + scheduleNextDeletion(pluginData); +} diff --git a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts new file mode 100644 index 00000000..593914df --- /dev/null +++ b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts @@ -0,0 +1,28 @@ +import { PluginData } from "knub"; +import { AutoDeletePluginType } from "../types"; +import moment from "moment-timezone"; +import { LogType } from "src/data/LogType"; +import { stripObjectToScalars, resolveUser } from "src/utils"; +import { logger } from "src/logger"; +import { scheduleNextDeletion } from "./scheduleNextDeletion"; + +export async function deleteNextItem(pluginData: PluginData) { + const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1); + if (!itemToDelete) return; + + pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id); + pluginData.client.deleteMessage(itemToDelete.message.channel_id, itemToDelete.message.id).catch(logger.warn); + + scheduleNextDeletion(pluginData); + + const user = await resolveUser(pluginData.client, itemToDelete.message.user_id); + const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id); + const messageDate = moment(itemToDelete.message.data.timestamp, "x").format("YYYY-MM-DD HH:mm:ss"); + + pluginData.state.guildLogs.log(LogType.MESSAGE_DELETE_AUTO, { + message: itemToDelete.message, + user: stripObjectToScalars(user), + channel: stripObjectToScalars(channel), + messageDate, + }); +} diff --git a/backend/src/plugins/AutoDelete/util/onMessageCreate.ts b/backend/src/plugins/AutoDelete/util/onMessageCreate.ts new file mode 100644 index 00000000..11dad95d --- /dev/null +++ b/backend/src/plugins/AutoDelete/util/onMessageCreate.ts @@ -0,0 +1,26 @@ +import { AutoDeletePluginType, MAX_DELAY } from "../types"; +import { PluginData } from "knub"; +import { SavedMessage } from "src/data/entities/SavedMessage"; +import { convertDelayStringToMS, resolveMember } from "src/utils"; +import { LogType } from "src/data/LogType"; +import { addMessageToDeletionQueue } from "./addMessageToDeletionQueue"; + +export async function onMessageCreate(pluginData: PluginData, msg: SavedMessage) { + const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id); + const config = pluginData.config.getMatchingConfig({ member, channelId: msg.channel_id }); + if (config.enabled) { + let delay = convertDelayStringToMS(config.delay); + + if (delay > MAX_DELAY) { + delay = MAX_DELAY; + if (!pluginData.state.maxDelayWarningSent) { + pluginData.state.guildLogs.log(LogType.BOT_ALERT, { + body: `Clamped auto-deletion delay in <#${msg.channel_id}> to 5 minutes`, + }); + pluginData.state.maxDelayWarningSent = true; + } + } + + addMessageToDeletionQueue(pluginData, msg, delay); + } +} diff --git a/backend/src/plugins/AutoDelete/util/onMessageDelete.ts b/backend/src/plugins/AutoDelete/util/onMessageDelete.ts new file mode 100644 index 00000000..e0e96382 --- /dev/null +++ b/backend/src/plugins/AutoDelete/util/onMessageDelete.ts @@ -0,0 +1,12 @@ +import { PluginData } from "knub"; +import { AutoDeletePluginType } from "../types"; +import { SavedMessage } from "src/data/entities/SavedMessage"; +import { scheduleNextDeletion } from "./scheduleNextDeletion"; + +export function onMessageDelete(pluginData: PluginData, msg: SavedMessage) { + const indexToDelete = pluginData.state.deletionQueue.findIndex(item => item.message.id === msg.id); + if (indexToDelete > -1) { + pluginData.state.deletionQueue.splice(indexToDelete, 1); + scheduleNextDeletion(pluginData); + } +} diff --git a/backend/src/plugins/AutoDelete/util/onMessageDeleteBulk.ts b/backend/src/plugins/AutoDelete/util/onMessageDeleteBulk.ts new file mode 100644 index 00000000..9993254c --- /dev/null +++ b/backend/src/plugins/AutoDelete/util/onMessageDeleteBulk.ts @@ -0,0 +1,10 @@ +import { AutoDeletePluginType } from "../types"; +import { PluginData } from "knub"; +import { SavedMessage } from "src/data/entities/SavedMessage"; +import { onMessageDelete } from "./onMessageDelete"; + +export function onMessageDeleteBulk(pluginData: PluginData, messages: SavedMessage[]) { + for (const msg of messages) { + onMessageDelete(pluginData, msg); + } +} diff --git a/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts b/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts new file mode 100644 index 00000000..5a9f2797 --- /dev/null +++ b/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts @@ -0,0 +1,14 @@ +import { PluginData } from "knub"; +import { AutoDeletePluginType } from "../types"; +import { deleteNextItem } from "./deleteNextItem"; + +export function scheduleNextDeletion(pluginData: PluginData) { + if (pluginData.state.deletionQueue.length === 0) { + clearTimeout(pluginData.state.nextDeletionTimeout); + return; + } + + const firstDeleteAt = pluginData.state.deletionQueue[0].deleteAt; + clearTimeout(pluginData.state.nextDeletionTimeout); + pluginData.state.nextDeletionTimeout = setTimeout(() => deleteNextItem(pluginData), firstDeleteAt - Date.now()); +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 32825725..2583acc5 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -13,9 +13,11 @@ import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigRelo import { CasesPlugin } from "./Cases/CasesPlugin"; import { MutesPlugin } from "./Mutes/MutesPlugin"; import { TagsPlugin } from "./Tags/TagsPlugin"; +import { AutoDeletePlugin } from "./AutoDelete/AutoDeletePlugin"; // prettier-ignore export const guildPlugins: Array> = [ + AutoDeletePlugin, AutoReactionsPlugin, LocateUserPlugin, PersistPlugin,