diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index 9c5ce269..3edffc37 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -15,6 +15,7 @@ import { MINUTES, SECONDS } from "../../utils"; import { clearOldRecentSpam } from "./functions/clearOldRecentSpam"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; import { GuildArchives } from "../../data/GuildArchives"; +import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChanges"; const defaultOptions = { config: { @@ -94,9 +95,9 @@ const configPreprocessor: ConfigPreprocessorFn = options => { } } - // if (rule["actions"]["log"] == null) { - // rule["actions"]["log"] = true; - // } + if (rule["actions"]["log"] == null) { + rule["actions"]["log"] = true; + } } } } @@ -123,6 +124,14 @@ export const AutomodPlugin = zeppelinPlugin()("automod", { pluginData.state.recentSpam = []; pluginData.state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS); + pluginData.state.recentNicknameChanges = new Map(); + pluginData.state.clearRecentNicknameChangesInterval = setInterval( + () => clearOldRecentNicknameChanges(pluginData), + 30 * SECONDS, + ); + + pluginData.state.cachedAntiraidLevel = null; // TODO + pluginData.state.logs = new GuildLogs(pluginData.guild.id); pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id); pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id); @@ -142,6 +151,8 @@ export const AutomodPlugin = zeppelinPlugin()("automod", { 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); }, diff --git a/backend/src/plugins/Automod/actions/addRoles.ts b/backend/src/plugins/Automod/actions/addRoles.ts new file mode 100644 index 00000000..47b55c2c --- /dev/null +++ b/backend/src/plugins/Automod/actions/addRoles.ts @@ -0,0 +1,35 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, resolveMember, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; + +export const AddRolesAction = automodAction({ + configType: t.array(t.string), + + async apply({ pluginData, contexts, actionConfig }) { + const members = contexts.map(c => c.member).filter(Boolean); + const uniqueMembers = new Set(members); + + await Promise.all( + Array.from(uniqueMembers.values()).map(async member => { + const memberRoles = new Set(member.roles); + for (const roleId of actionConfig) { + memberRoles.add(roleId); + } + + if (memberRoles.size === member.roles.length) { + // No role changes + return; + } + + const rolesArr = Array.from(memberRoles.values()); + await member.edit({ + roles: rolesArr, + }); + member.roles = rolesArr; // Make sure we know of the new roles internally as well + }), + ); + }, +}); diff --git a/backend/src/plugins/Automod/actions/alert.ts b/backend/src/plugins/Automod/actions/alert.ts new file mode 100644 index 00000000..a14a2843 --- /dev/null +++ b/backend/src/plugins/Automod/actions/alert.ts @@ -0,0 +1,48 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, messageLink, resolveMember, stripObjectToScalars, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { TextChannel } from "eris"; +import { renderTemplate } from "../../../templateFormatter"; + +export const AlertAction = automodAction({ + configType: t.type({ + channel: t.string, + text: t.string, + }), + + async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { + const channel = pluginData.guild.channels.get(actionConfig.channel); + + if (channel && channel instanceof TextChannel) { + const text = actionConfig.text; + const theMessageLink = + contexts[0].message && messageLink(pluginData.guild.id, contexts[0].message.channel_id, contexts[0].message.id); + + const safeUsers = contexts.map(c => c.user && stripObjectToScalars(c.user)).filter(Boolean); + const safeUser = safeUsers[0]; + + const takenActions = Object.keys(pluginData.config.get().rules[ruleName].actions); + // TODO: Generate logMessage + const logMessage = ""; + + const rendered = await renderTemplate(actionConfig.text, { + rule: ruleName, + user: safeUser, + users: safeUsers, + text, + matchSummary: matchResult.summary, + messageLink: theMessageLink, + logMessage, + }); + channel.createMessage(rendered); + } else { + // TODO: Post BOT_ALERT log + /*this.getLogs().log(LogType.BOT_ALERT, { + body: `Invalid channel id \`${actionConfig.channel}\` for alert action in automod rule **${rule.name}**`, + });*/ + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts new file mode 100644 index 00000000..8f433023 --- /dev/null +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -0,0 +1,35 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, resolveMember, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; + +export const BanAction = automodAction({ + configType: t.type({ + reason: tNullable(t.string), + notify: tNullable(t.string), + notifyChannel: tNullable(t.string), + deleteMessageDays: tNullable(t.number), + }), + + async apply({ pluginData, contexts, actionConfig }) { + const reason = actionConfig.reason || "Kicked automatically"; + const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + const deleteMessageDays = actionConfig.deleteMessageDays; + + const caseArgs = { + modId: pluginData.client.user.id, + extraNotes: [ + /* TODO */ + ], + }; + + const userIdsToBan = contexts.map(c => c.user?.id).filter(Boolean); + + const modActions = pluginData.getPlugin(ModActionsPlugin); + for (const userId of userIdsToBan) { + await modActions.banUserId(userId, reason, { contactMethods, caseArgs, deleteMessageDays }); + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/changeNickname.ts b/backend/src/plugins/Automod/actions/changeNickname.ts new file mode 100644 index 00000000..061bf98a --- /dev/null +++ b/backend/src/plugins/Automod/actions/changeNickname.ts @@ -0,0 +1,27 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, resolveMember, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; + +export const ChangeNicknameAction = automodAction({ + configType: t.type({ + name: t.string, + }), + + async apply({ pluginData, contexts, actionConfig }) { + const members = contexts.map(c => c.member).filter(Boolean); + const uniqueMembers = new Set(members); + + for (const member of uniqueMembers) { + if (pluginData.state.recentNicknameChanges.has(member.id)) continue; + + member.edit({ nick: actionConfig.name }).catch(err => { + /* TODO: Log this error */ + }); + + pluginData.state.recentNicknameChanges.set(member.id, { timestamp: Date.now() }); + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/exampleAction.ts b/backend/src/plugins/Automod/actions/exampleAction.ts new file mode 100644 index 00000000..ba5c0e27 --- /dev/null +++ b/backend/src/plugins/Automod/actions/exampleAction.ts @@ -0,0 +1,12 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; + +export const ExampleAction = automodAction({ + configType: t.type({ + someValue: t.string, + }), + + async apply({ pluginData, contexts, actionConfig }) { + // TODO: Everything + }, +}); diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts new file mode 100644 index 00000000..09cb832f --- /dev/null +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -0,0 +1,34 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, resolveMember, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; + +export const KickAction = automodAction({ + configType: t.type({ + reason: tNullable(t.string), + notify: tNullable(t.string), + notifyChannel: tNullable(t.string), + }), + + async apply({ pluginData, contexts, actionConfig }) { + const reason = actionConfig.reason || "Kicked automatically"; + const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + + const caseArgs = { + modId: pluginData.client.user.id, + extraNotes: [ + /* TODO */ + ], + }; + + const userIdsToKick = contexts.map(c => c.user?.id).filter(Boolean); + const membersToKick = await asyncMap(userIdsToKick, id => resolveMember(pluginData.client, pluginData.guild, id)); + + const modActions = pluginData.getPlugin(ModActionsPlugin); + for (const member of membersToKick) { + await modActions.kickMember(member, reason, { contactMethods, caseArgs }); + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/log.ts b/backend/src/plugins/Automod/actions/log.ts new file mode 100644 index 00000000..a2038412 --- /dev/null +++ b/backend/src/plugins/Automod/actions/log.ts @@ -0,0 +1,10 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; + +export const LogAction = automodAction({ + configType: t.boolean, + + async apply({ pluginData, contexts, actionConfig }) { + // TODO: Everything + }, +}); diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts new file mode 100644 index 00000000..bf67a269 --- /dev/null +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -0,0 +1,36 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, convertDelayStringToMS, resolveMember, tDelayString, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { MutesPlugin } from "../../Mutes/MutesPlugin"; + +export const MuteAction = automodAction({ + configType: t.type({ + reason: tNullable(t.string), + duration: tNullable(tDelayString), + notify: tNullable(t.string), + notifyChannel: tNullable(t.string), + }), + + async apply({ pluginData, contexts, actionConfig }) { + const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration) : null; + const reason = actionConfig.reason || "Muted automatically"; + const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + + const caseArgs = { + modId: pluginData.client.user.id, + extraNotes: [ + /* TODO */ + ], + }; + + const userIdsToMute = contexts.map(c => c.user?.id).filter(Boolean); + + const mutes = pluginData.getPlugin(MutesPlugin); + for (const userId of userIdsToMute) { + await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs }); + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/removeRoles.ts b/backend/src/plugins/Automod/actions/removeRoles.ts new file mode 100644 index 00000000..fd27bb1d --- /dev/null +++ b/backend/src/plugins/Automod/actions/removeRoles.ts @@ -0,0 +1,35 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, resolveMember, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; + +export const RemoveRolesAction = automodAction({ + configType: t.array(t.string), + + async apply({ pluginData, contexts, actionConfig }) { + const members = contexts.map(c => c.member).filter(Boolean); + const uniqueMembers = new Set(members); + + await Promise.all( + Array.from(uniqueMembers.values()).map(async member => { + const memberRoles = new Set(member.roles); + for (const roleId of actionConfig) { + memberRoles.delete(roleId); + } + + if (memberRoles.size === member.roles.length) { + // No role changes + return; + } + + const rolesArr = Array.from(memberRoles.values()); + await member.edit({ + roles: rolesArr, + }); + member.roles = rolesArr; // Make sure we know of the new roles internally as well + }), + ); + }, +}); diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts new file mode 100644 index 00000000..85353820 --- /dev/null +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -0,0 +1,63 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { + convertDelayStringToMS, + noop, + renderRecursively, + stripObjectToScalars, + tDelayString, + tMessageContent, + tNullable, +} from "../../../utils"; +import { TextChannel } from "eris"; +import { AutomodContext } from "../types"; +import { renderTemplate } from "../../../templateFormatter"; + +export const ReplyAction = automodAction({ + configType: t.union([ + t.string, + t.type({ + text: tMessageContent, + auto_delete: tNullable(t.union([tDelayString, t.number])), + }), + ]), + + async apply({ pluginData, contexts, actionConfig }) { + const contextsWithTextChannels = contexts + .filter(c => c.message?.channel_id) + .filter(c => pluginData.guild.channels.get(c.message.channel_id) instanceof TextChannel); + + const contextsByChannelId = contextsWithTextChannels.reduce((map: Map, context) => { + if (!map.has(context.message.channel_id)) { + map.set(context.message.channel_id, []); + } + + map.get(context.message.channel_id).push(context); + return map; + }, new Map()); + + for (const [channelId, _contexts] of contextsByChannelId.entries()) { + const users = Array.from(new Set(_contexts.map(c => c.user).filter(Boolean))); + const user = users[0]; + + const renderReplyText = async str => + renderTemplate(str, { + user: stripObjectToScalars(user), + }); + const formatted = + typeof actionConfig === "string" + ? await renderReplyText(actionConfig) + : await renderRecursively(actionConfig.text, renderReplyText); + + if (formatted) { + const channel = pluginData.guild.channels.get(channelId) as TextChannel; + const replyMsg = await channel.createMessage(formatted); + + if (typeof actionConfig === "object" && actionConfig.auto_delete) { + const delay = convertDelayStringToMS(String(actionConfig.auto_delete)); + setTimeout(() => replyMsg.delete().catch(noop), delay); + } + } + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/setAntiraidLevel.ts b/backend/src/plugins/Automod/actions/setAntiraidLevel.ts new file mode 100644 index 00000000..9a7ccc0e --- /dev/null +++ b/backend/src/plugins/Automod/actions/setAntiraidLevel.ts @@ -0,0 +1,11 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { setAntiraidLevel } from "../functions/setAntiraidLevel"; + +export const SetAntiraidLevelAction = automodAction({ + configType: t.string, + + async apply({ pluginData, contexts, actionConfig }) { + setAntiraidLevel(pluginData, actionConfig); + }, +}); diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts new file mode 100644 index 00000000..2062d2a2 --- /dev/null +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -0,0 +1,34 @@ +import * as t from "io-ts"; +import { automodAction } from "../helpers"; +import { LogType } from "../../../data/LogType"; +import { asyncMap, resolveMember, tNullable } from "../../../utils"; +import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; + +export const WarnAction = automodAction({ + configType: t.type({ + reason: tNullable(t.string), + notify: tNullable(t.string), + notifyChannel: tNullable(t.string), + }), + + async apply({ pluginData, contexts, actionConfig }) { + const reason = actionConfig.reason || "Warned automatically"; + const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + + const caseArgs = { + modId: pluginData.client.user.id, + extraNotes: [ + /* TODO */ + ], + }; + + const userIdsToWarn = contexts.map(c => c.user?.id).filter(Boolean); + const membersToWarn = await asyncMap(userIdsToWarn, id => resolveMember(pluginData.client, pluginData.guild, id)); + + const modActions = pluginData.getPlugin(ModActionsPlugin); + for (const member of membersToWarn) { + await modActions.warnMember(member, reason, { contactMethods, caseArgs }); + } + }, +}); diff --git a/backend/src/plugins/Automod/constants.ts b/backend/src/plugins/Automod/constants.ts index 3ebc278c..a2445c3e 100644 --- a/backend/src/plugins/Automod/constants.ts +++ b/backend/src/plugins/Automod/constants.ts @@ -2,6 +2,7 @@ import { MINUTES, SECONDS } from "../../utils"; export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS; export const RECENT_ACTION_EXPIRY_TIME = 5 * MINUTES; +export const RECENT_NICKNAME_CHANGE_EXPIRY_TIME = 5 * MINUTES; export enum RecentActionType { Message = 1, diff --git a/backend/src/plugins/Automod/functions/clearOldNicknameChanges.ts b/backend/src/plugins/Automod/functions/clearOldNicknameChanges.ts new file mode 100644 index 00000000..2f503cf8 --- /dev/null +++ b/backend/src/plugins/Automod/functions/clearOldNicknameChanges.ts @@ -0,0 +1,12 @@ +import { PluginData } from "knub"; +import { AutomodPluginType } from "../types"; +import { RECENT_NICKNAME_CHANGE_EXPIRY_TIME, RECENT_SPAM_EXPIRY_TIME } from "../constants"; + +export function clearOldRecentNicknameChanges(pluginData: PluginData) { + const now = Date.now(); + for (const [userId, { timestamp }] of pluginData.state.recentNicknameChanges) { + if (timestamp + RECENT_NICKNAME_CHANGE_EXPIRY_TIME <= now) { + pluginData.state.recentNicknameChanges.delete(userId); + } + } +} diff --git a/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts b/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts new file mode 100644 index 00000000..6ae3fca0 --- /dev/null +++ b/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts @@ -0,0 +1,32 @@ +import { disableUserNotificationStrings, UserNotificationMethod } from "../../../utils"; +import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; +import { TextChannel } from "eris"; +import { PluginData } from "knub"; +import { AutomodPluginType } from "../types"; + +export function resolveActionContactMethods( + pluginData: PluginData, + actionConfig: { + notify?: string; + notifyChannel?: string; + }, +): UserNotificationMethod[] | null { + if (actionConfig.notify === "dm") { + return [{ type: "dm" }]; + } else if (actionConfig.notify === "channel") { + if (!actionConfig.notifyChannel) { + throw new RecoverablePluginError(ERRORS.NO_USER_NOTIFICATION_CHANNEL); + } + + const channel = pluginData.guild.channels.get(actionConfig.notifyChannel); + if (!(channel instanceof TextChannel)) { + throw new RecoverablePluginError(ERRORS.INVALID_USER_NOTIFICATION_CHANNEL); + } + + return [{ type: "channel", channel }]; + } else if (actionConfig.notify && disableUserNotificationStrings.includes(actionConfig.notify)) { + return []; + } + + return null; +} diff --git a/backend/src/plugins/Automod/functions/runAutomod.ts b/backend/src/plugins/Automod/functions/runAutomod.ts index f3bcde40..428acf6e 100644 --- a/backend/src/plugins/Automod/functions/runAutomod.ts +++ b/backend/src/plugins/Automod/functions/runAutomod.ts @@ -25,7 +25,6 @@ export async function runAutomod(pluginData: PluginData, cont if (!rule.affects_bots && user.bot) continue; let matchResult: AutomodTriggerMatchResult; - let matchSummary: string; let contexts: AutomodContext[]; triggerLoop: for (const triggerItem of rule.triggers) { @@ -45,17 +44,7 @@ export async function runAutomod(pluginData: PluginData, cont _context.actioned = true; } - if (matchResult.silentClean) { - await CleanAction.apply({ - ruleName, - pluginData, - contexts, - actionConfig: true, - }); - return; - } - - matchSummary = await trigger.renderMatchInformation({ + matchResult.summary = await trigger.renderMatchInformation({ ruleName, pluginData, contexts, @@ -63,6 +52,17 @@ export async function runAutomod(pluginData: PluginData, cont triggerConfig, }); + if (matchResult.silentClean) { + await CleanAction.apply({ + ruleName, + pluginData, + contexts, + actionConfig: true, + matchResult, + }); + return; + } + break triggerLoop; } } @@ -76,6 +76,7 @@ export async function runAutomod(pluginData: PluginData, cont pluginData, contexts, actionConfig, + matchResult, }); } diff --git a/backend/src/plugins/Automod/functions/setAntiraidLevel.ts b/backend/src/plugins/Automod/functions/setAntiraidLevel.ts new file mode 100644 index 00000000..913f66e5 --- /dev/null +++ b/backend/src/plugins/Automod/functions/setAntiraidLevel.ts @@ -0,0 +1,18 @@ +import { User } from "eris"; +import { PluginData } from "knub"; +import { AutomodPluginType } from "../types"; + +export async function setAntiraidLevel( + pluginData: PluginData, + newLevel: string | null, + user?: User, +) { + pluginData.state.cachedAntiraidLevel = newLevel; + await pluginData.state.antiraidLevels.set(newLevel); + + if (user) { + // TODO: Log user action + } else { + // TODO: Log automatic action + } +} diff --git a/backend/src/plugins/Automod/helpers.ts b/backend/src/plugins/Automod/helpers.ts index e654e1ef..0dcce48f 100644 --- a/backend/src/plugins/Automod/helpers.ts +++ b/backend/src/plugins/Automod/helpers.ts @@ -8,6 +8,8 @@ export interface AutomodTriggerMatchResult { extra?: TExtra; silentClean?: boolean; // TODO: Maybe generalize to a "silent" value in general, which mutes alert/log + + summary?: string; } type AutomodTriggerMatchFn = (meta: { @@ -54,6 +56,7 @@ type AutomodActionApplyFn = (meta: { pluginData: PluginData; contexts: AutomodContext[]; actionConfig: TConfigType; + matchResult: AutomodTriggerMatchResult; }) => Awaitable; export interface AutomodActionBlueprint { diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 43c95031..f77028e6 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -55,6 +55,11 @@ export interface AutomodPluginType extends BasePluginType { recentSpam: RecentSpam[]; clearRecentSpamInterval: Timeout; + recentNicknameChanges: Map; + clearRecentNicknameChangesInterval: Timeout; + + cachedAntiraidLevel: string | null; + savedMessages: GuildSavedMessages; logs: GuildLogs; antiraidLevels: GuildAntiraidLevels; diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index fe918678..86a589a7 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -1,7 +1,7 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; import { CasesPlugin } from "../Cases/CasesPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin"; -import { ConfigSchema, ModActionsPluginType } from "./types"; +import { BanOptions, ConfigSchema, KickOptions, ModActionsPluginType, WarnOptions } from "./types"; import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt"; import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt"; import { CreateKickCaseOnManualKickEvt } from "./events/CreateKickCaseOnManualKickEvt"; @@ -28,6 +28,10 @@ import { GuildMutes } from "src/data/GuildMutes"; import { GuildCases } from "src/data/GuildCases"; import { GuildLogs } from "src/data/GuildLogs"; import { ForceUnmuteCmd } from "./commands/ForceunmuteCmd"; +import { warnMember } from "./functions/warnMember"; +import { Member } from "eris"; +import { kickMember } from "./functions/kickMember"; +import { banUserId } from "./functions/banUserId"; const defaultOptions = { config: { @@ -119,6 +123,26 @@ export const ModActionsPlugin = zeppelinPlugin()("mod_acti UnhideCaseCmd, ], + public: { + warnMember(pluginData) { + return (member: Member, reason: string, warnOptions?: WarnOptions) => { + warnMember(pluginData, member, reason, warnOptions); + }; + }, + + kickMember(pluginData) { + return (member: Member, reason: string, kickOptions?: KickOptions) => { + kickMember(pluginData, member, reason, kickOptions); + }; + }, + + banUserId(pluginData) { + return (userId: string, reason?: string, banOptions?: BanOptions) => { + banUserId(pluginData, userId, reason, banOptions); + }; + }, + }, + onLoad(pluginData) { const { state, guild } = pluginData; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 2f45fb7f..831129f4 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -34,6 +34,7 @@ import { either } from "fp-ts/lib/Either"; import moment from "moment-timezone"; import { SimpleCache } from "./SimpleCache"; import { logger } from "./logger"; +import { Awaitable } from "knub/dist/utils"; const fsp = fs.promises; @@ -1222,3 +1223,7 @@ export function isFullMessage(msg: PossiblyUncachedMessage): msg is Message { export function isGuildInvite(invite: AnyInvite): invite is GuildInvite { return (invite as GuildInvite).guild != null; } + +export function asyncMap(arr: T[], fn: (item: T) => Promise): Promise { + return Promise.all(arr.map((item, index) => fn(item))); +}