import { configUtils, CooldownManager } from "knub"; import { ConfigPreprocessorFn } from "knub/dist/config/configTypes"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { Queue } from "../../Queue"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { MINUTES, SECONDS } from "../../utils"; import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap"; import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap"; import { StrictValidationError } from "../../validatorUtils"; import { CountersPlugin } from "../Counters/CountersPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { ModActionsPlugin } from "../ModActions/ModActionsPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { availableActions } from "./actions/availableActions"; import { AntiraidClearCmd } from "./commands/AntiraidClearCmd"; import { SetAntiraidCmd } from "./commands/SetAntiraidCmd"; import { ViewAntiraidCmd } from "./commands/ViewAntiraidCmd"; import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger"; import { RunAutomodOnJoinEvt, RunAutomodOnLeaveEvt } from "./events/RunAutomodOnJoinLeaveEvt"; import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate"; import { runAutomodOnMessage } from "./events/runAutomodOnMessage"; import { runAutomodOnModAction } from "./events/runAutomodOnModAction"; import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChanges"; import { clearOldRecentActions } from "./functions/clearOldRecentActions"; import { clearOldRecentSpam } from "./functions/clearOldRecentSpam"; import { pluginInfo } from "./info"; import { availableTriggers } from "./triggers/availableTriggers"; import { AutomodPluginType, ConfigSchema } from "./types"; const defaultOptions = { config: { rules: {}, antiraid_levels: ["low", "medium", "high"], can_set_antiraid: false, can_view_antiraid: false, }, overrides: [ { level: ">=50", config: { can_view_antiraid: true, }, }, { level: ">=100", config: { can_set_antiraid: true, }, }, ], }; /** * Config preprocessor to set default values for triggers and perform extra validation */ const configPreprocessor: ConfigPreprocessorFn = options => { if (options.config?.rules) { // Loop through each rule for (const [name, rule] of Object.entries(options.config.rules)) { if (rule == null) { delete options.config.rules[name]; continue; } rule["name"] = name; // If the rule doesn't have an explicitly set "enabled" property, set it to true if (rule["enabled"] == null) { rule["enabled"] = true; } if (rule["allow_further_rules"] == null) { rule["allow_further_rules"] = false; } if (rule["affects_bots"] == null) { rule["affects_bots"] = false; } // Loop through the rule's triggers if (rule["triggers"]) { for (const triggerObj of rule["triggers"]) { for (const triggerName in triggerObj) { if (!availableTriggers[triggerName]) { throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule.name}'`]); } const triggerBlueprint = availableTriggers[triggerName]; if (typeof triggerBlueprint.defaultConfig === "object" && triggerBlueprint.defaultConfig != null) { triggerObj[triggerName] = configUtils.mergeConfig( triggerBlueprint.defaultConfig, triggerObj[triggerName] || {}, ); } else { triggerObj[triggerName] = triggerObj[triggerName] || triggerBlueprint.defaultConfig; } if (triggerObj[triggerName].match_attachment_type) { const white = triggerObj[triggerName].match_attachment_type.whitelist_enabled; const black = triggerObj[triggerName].match_attachment_type.blacklist_enabled; if (white && black) { throw new StrictValidationError([ `Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_attachment_type>`, ]); } else if (!white && !black) { throw new StrictValidationError([ `Must have either blacklist or whitelist enabled at rule <${rule.name}/match_attachment_type>`, ]); } } if (triggerObj[triggerName].match_mime_type) { const white = triggerObj[triggerName].match_mime_type.whitelist_enabled; const black = triggerObj[triggerName].match_mime_type.blacklist_enabled; if (white && black) { throw new StrictValidationError([ `Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_mime_type>`, ]); } else if (!white && !black) { throw new StrictValidationError([ `Must have either blacklist or whitelist enabled at rule <${rule.name}/match_mime_type>`, ]); } } } } } if (rule["actions"]) { for (const actionName in rule["actions"]) { if (!availableActions[actionName]) { throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]); } const actionBlueprint = availableActions[actionName]; const actionConfig = rule["actions"][actionName]; if (typeof actionConfig !== "object" || Array.isArray(actionConfig) || actionConfig == null) { rule["actions"][actionName] = actionConfig; } else { rule["actions"][actionName] = configUtils.mergeConfig(actionBlueprint.defaultConfig, actionConfig); } } } // Enable logging of automod actions by default if (rule["actions"]) { for (const actionName in rule.actions) { if (!availableActions[actionName]) { throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]); } } if (rule["actions"]["log"] == null) { rule["actions"]["log"] = true; } } } } return options; }; export const AutomodPlugin = zeppelinGuildPlugin()({ name: "automod", showInDocs: true, info: pluginInfo, // prettier-ignore dependencies: () => [ LogsPlugin, ModActionsPlugin, MutesPlugin, CountersPlugin, ], configSchema: ConfigSchema, defaultOptions, configPreprocessor, customOverrideCriteriaFunctions: { antiraid_level: (pluginData, matchParams, value) => { return value ? value === pluginData.state.cachedAntiraidLevel : false; }, }, // prettier-ignore events: [ RunAutomodOnJoinEvt, RunAutomodOnMemberUpdate, RunAutomodOnLeaveEvt, // Messages use message events from SavedMessages, see onLoad below ], commands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd], async beforeLoad(pluginData) { pluginData.state.queue = new Queue(); pluginData.state.regexRunner = getRegExpRunner(`guild-${pluginData.guild.id}`); pluginData.state.recentActions = []; pluginData.state.recentSpam = []; pluginData.state.recentNicknameChanges = new Map(); pluginData.state.ignoredRoleChanges = new Set(); pluginData.state.cooldownManager = new CooldownManager(); pluginData.state.logs = new GuildLogs(pluginData.guild.id); pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id); pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id); pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id); pluginData.state.cachedAntiraidLevel = await pluginData.state.antiraidLevels.get(); }, async afterLoad(pluginData) { pluginData.state.clearRecentActionsInterval = setInterval(() => clearOldRecentActions(pluginData), 1 * MINUTES); pluginData.state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS); pluginData.state.clearRecentNicknameChangesInterval = setInterval( () => clearOldRecentNicknameChanges(pluginData), 30 * SECONDS, ); pluginData.state.onMessageCreateFn = message => runAutomodOnMessage(pluginData, message, false); pluginData.state.savedMessages.events.on("create", pluginData.state.onMessageCreateFn); pluginData.state.onMessageUpdateFn = message => runAutomodOnMessage(pluginData, message, true); pluginData.state.savedMessages.events.on("update", pluginData.state.onMessageUpdateFn); const countersPlugin = pluginData.getPlugin(CountersPlugin); pluginData.state.onCounterTrigger = (name, triggerName, channelId, userId) => { runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, false); }; pluginData.state.onCounterReverseTrigger = (name, triggerName, channelId, userId) => { runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, true); }; countersPlugin.onCounterEvent("trigger", pluginData.state.onCounterTrigger); countersPlugin.onCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger); const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter(); pluginData.state.modActionsListeners = new Map(); pluginData.state.modActionsListeners.set("note", (userId: string) => runAutomodOnModAction(pluginData, "note", 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, reason: string | undefined, isAutomodAction: boolean) => runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction), ); 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), ); registerEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners); const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter(); pluginData.state.mutesListeners = new Map(); 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), ); registerEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners); }, async beforeUnload(pluginData) { const countersPlugin = pluginData.getPlugin(CountersPlugin); countersPlugin.offCounterEvent("trigger", pluginData.state.onCounterTrigger); countersPlugin.offCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger); const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter(); unregisterEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners); const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter(); unregisterEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners); pluginData.state.queue.clear(); discardRegExpRunner(`guild-${pluginData.guild.id}`); clearInterval(pluginData.state.clearRecentActionsInterval); clearInterval(pluginData.state.clearRecentSpamInterval); clearInterval(pluginData.state.clearRecentNicknameChangesInterval); pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn); pluginData.state.savedMessages.events.off("update", pluginData.state.onMessageUpdateFn); }, });