mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-16 22:21:51 +00:00
Automod actions + ModActions public interface
This commit is contained in:
parent
0f0728bc1c
commit
86023877a2
22 changed files with 508 additions and 16 deletions
|
@ -15,6 +15,7 @@ import { MINUTES, SECONDS } from "../../utils";
|
||||||
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
|
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
|
||||||
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
|
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
|
||||||
import { GuildArchives } from "../../data/GuildArchives";
|
import { GuildArchives } from "../../data/GuildArchives";
|
||||||
|
import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChanges";
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
config: {
|
config: {
|
||||||
|
@ -94,9 +95,9 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = options => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (rule["actions"]["log"] == null) {
|
if (rule["actions"]["log"] == null) {
|
||||||
// rule["actions"]["log"] = true;
|
rule["actions"]["log"] = true;
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +124,14 @@ export const AutomodPlugin = zeppelinPlugin<AutomodPluginType>()("automod", {
|
||||||
pluginData.state.recentSpam = [];
|
pluginData.state.recentSpam = [];
|
||||||
pluginData.state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS);
|
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.logs = new GuildLogs(pluginData.guild.id);
|
||||||
pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id);
|
pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id);
|
||||||
pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id);
|
pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id);
|
||||||
|
@ -142,6 +151,8 @@ export const AutomodPlugin = zeppelinPlugin<AutomodPluginType>()("automod", {
|
||||||
|
|
||||||
clearInterval(pluginData.state.clearRecentSpamInterval);
|
clearInterval(pluginData.state.clearRecentSpamInterval);
|
||||||
|
|
||||||
|
clearInterval(pluginData.state.clearRecentNicknameChangesInterval);
|
||||||
|
|
||||||
pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn);
|
pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn);
|
||||||
pluginData.state.savedMessages.events.off("update", pluginData.state.onMessageUpdateFn);
|
pluginData.state.savedMessages.events.off("update", pluginData.state.onMessageUpdateFn);
|
||||||
},
|
},
|
||||||
|
|
35
backend/src/plugins/Automod/actions/addRoles.ts
Normal file
35
backend/src/plugins/Automod/actions/addRoles.ts
Normal file
|
@ -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
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
48
backend/src/plugins/Automod/actions/alert.ts
Normal file
48
backend/src/plugins/Automod/actions/alert.ts
Normal file
|
@ -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}**`,
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
35
backend/src/plugins/Automod/actions/ban.ts
Normal file
35
backend/src/plugins/Automod/actions/ban.ts
Normal file
|
@ -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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
27
backend/src/plugins/Automod/actions/changeNickname.ts
Normal file
27
backend/src/plugins/Automod/actions/changeNickname.ts
Normal file
|
@ -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() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
12
backend/src/plugins/Automod/actions/exampleAction.ts
Normal file
12
backend/src/plugins/Automod/actions/exampleAction.ts
Normal file
|
@ -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
|
||||||
|
},
|
||||||
|
});
|
34
backend/src/plugins/Automod/actions/kick.ts
Normal file
34
backend/src/plugins/Automod/actions/kick.ts
Normal file
|
@ -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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
10
backend/src/plugins/Automod/actions/log.ts
Normal file
10
backend/src/plugins/Automod/actions/log.ts
Normal file
|
@ -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
|
||||||
|
},
|
||||||
|
});
|
36
backend/src/plugins/Automod/actions/mute.ts
Normal file
36
backend/src/plugins/Automod/actions/mute.ts
Normal file
|
@ -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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
35
backend/src/plugins/Automod/actions/removeRoles.ts
Normal file
35
backend/src/plugins/Automod/actions/removeRoles.ts
Normal file
|
@ -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
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
63
backend/src/plugins/Automod/actions/reply.ts
Normal file
63
backend/src/plugins/Automod/actions/reply.ts
Normal file
|
@ -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<string, AutomodContext[]>, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
11
backend/src/plugins/Automod/actions/setAntiraidLevel.ts
Normal file
11
backend/src/plugins/Automod/actions/setAntiraidLevel.ts
Normal file
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
34
backend/src/plugins/Automod/actions/warn.ts
Normal file
34
backend/src/plugins/Automod/actions/warn.ts
Normal file
|
@ -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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ import { MINUTES, SECONDS } from "../../utils";
|
||||||
|
|
||||||
export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
|
export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
|
||||||
export const RECENT_ACTION_EXPIRY_TIME = 5 * MINUTES;
|
export const RECENT_ACTION_EXPIRY_TIME = 5 * MINUTES;
|
||||||
|
export const RECENT_NICKNAME_CHANGE_EXPIRY_TIME = 5 * MINUTES;
|
||||||
|
|
||||||
export enum RecentActionType {
|
export enum RecentActionType {
|
||||||
Message = 1,
|
Message = 1,
|
||||||
|
|
|
@ -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<AutomodPluginType>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AutomodPluginType>,
|
||||||
|
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;
|
||||||
|
}
|
|
@ -25,7 +25,6 @@ export async function runAutomod(pluginData: PluginData<AutomodPluginType>, cont
|
||||||
if (!rule.affects_bots && user.bot) continue;
|
if (!rule.affects_bots && user.bot) continue;
|
||||||
|
|
||||||
let matchResult: AutomodTriggerMatchResult<any>;
|
let matchResult: AutomodTriggerMatchResult<any>;
|
||||||
let matchSummary: string;
|
|
||||||
let contexts: AutomodContext[];
|
let contexts: AutomodContext[];
|
||||||
|
|
||||||
triggerLoop: for (const triggerItem of rule.triggers) {
|
triggerLoop: for (const triggerItem of rule.triggers) {
|
||||||
|
@ -45,17 +44,7 @@ export async function runAutomod(pluginData: PluginData<AutomodPluginType>, cont
|
||||||
_context.actioned = true;
|
_context.actioned = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchResult.silentClean) {
|
matchResult.summary = await trigger.renderMatchInformation({
|
||||||
await CleanAction.apply({
|
|
||||||
ruleName,
|
|
||||||
pluginData,
|
|
||||||
contexts,
|
|
||||||
actionConfig: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
matchSummary = await trigger.renderMatchInformation({
|
|
||||||
ruleName,
|
ruleName,
|
||||||
pluginData,
|
pluginData,
|
||||||
contexts,
|
contexts,
|
||||||
|
@ -63,6 +52,17 @@ export async function runAutomod(pluginData: PluginData<AutomodPluginType>, cont
|
||||||
triggerConfig,
|
triggerConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (matchResult.silentClean) {
|
||||||
|
await CleanAction.apply({
|
||||||
|
ruleName,
|
||||||
|
pluginData,
|
||||||
|
contexts,
|
||||||
|
actionConfig: true,
|
||||||
|
matchResult,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
break triggerLoop;
|
break triggerLoop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,7 @@ export async function runAutomod(pluginData: PluginData<AutomodPluginType>, cont
|
||||||
pluginData,
|
pluginData,
|
||||||
contexts,
|
contexts,
|
||||||
actionConfig,
|
actionConfig,
|
||||||
|
matchResult,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
backend/src/plugins/Automod/functions/setAntiraidLevel.ts
Normal file
18
backend/src/plugins/Automod/functions/setAntiraidLevel.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { User } from "eris";
|
||||||
|
import { PluginData } from "knub";
|
||||||
|
import { AutomodPluginType } from "../types";
|
||||||
|
|
||||||
|
export async function setAntiraidLevel(
|
||||||
|
pluginData: PluginData<AutomodPluginType>,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ export interface AutomodTriggerMatchResult<TExtra extends any = unknown> {
|
||||||
extra?: TExtra;
|
extra?: TExtra;
|
||||||
|
|
||||||
silentClean?: boolean; // TODO: Maybe generalize to a "silent" value in general, which mutes alert/log
|
silentClean?: boolean; // TODO: Maybe generalize to a "silent" value in general, which mutes alert/log
|
||||||
|
|
||||||
|
summary?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutomodTriggerMatchFn<TConfigType, TMatchResultExtra> = (meta: {
|
type AutomodTriggerMatchFn<TConfigType, TMatchResultExtra> = (meta: {
|
||||||
|
@ -54,6 +56,7 @@ type AutomodActionApplyFn<TConfigType> = (meta: {
|
||||||
pluginData: PluginData<AutomodPluginType>;
|
pluginData: PluginData<AutomodPluginType>;
|
||||||
contexts: AutomodContext[];
|
contexts: AutomodContext[];
|
||||||
actionConfig: TConfigType;
|
actionConfig: TConfigType;
|
||||||
|
matchResult: AutomodTriggerMatchResult;
|
||||||
}) => Awaitable<void>;
|
}) => Awaitable<void>;
|
||||||
|
|
||||||
export interface AutomodActionBlueprint<TConfigType extends t.Any> {
|
export interface AutomodActionBlueprint<TConfigType extends t.Any> {
|
||||||
|
|
|
@ -55,6 +55,11 @@ export interface AutomodPluginType extends BasePluginType {
|
||||||
recentSpam: RecentSpam[];
|
recentSpam: RecentSpam[];
|
||||||
clearRecentSpamInterval: Timeout;
|
clearRecentSpamInterval: Timeout;
|
||||||
|
|
||||||
|
recentNicknameChanges: Map<string, { timestamp: number }>;
|
||||||
|
clearRecentNicknameChangesInterval: Timeout;
|
||||||
|
|
||||||
|
cachedAntiraidLevel: string | null;
|
||||||
|
|
||||||
savedMessages: GuildSavedMessages;
|
savedMessages: GuildSavedMessages;
|
||||||
logs: GuildLogs;
|
logs: GuildLogs;
|
||||||
antiraidLevels: GuildAntiraidLevels;
|
antiraidLevels: GuildAntiraidLevels;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
import { CasesPlugin } from "../Cases/CasesPlugin";
|
import { CasesPlugin } from "../Cases/CasesPlugin";
|
||||||
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
||||||
import { ConfigSchema, ModActionsPluginType } from "./types";
|
import { BanOptions, ConfigSchema, KickOptions, ModActionsPluginType, WarnOptions } from "./types";
|
||||||
import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt";
|
import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt";
|
||||||
import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt";
|
import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt";
|
||||||
import { CreateKickCaseOnManualKickEvt } from "./events/CreateKickCaseOnManualKickEvt";
|
import { CreateKickCaseOnManualKickEvt } from "./events/CreateKickCaseOnManualKickEvt";
|
||||||
|
@ -28,6 +28,10 @@ import { GuildMutes } from "src/data/GuildMutes";
|
||||||
import { GuildCases } from "src/data/GuildCases";
|
import { GuildCases } from "src/data/GuildCases";
|
||||||
import { GuildLogs } from "src/data/GuildLogs";
|
import { GuildLogs } from "src/data/GuildLogs";
|
||||||
import { ForceUnmuteCmd } from "./commands/ForceunmuteCmd";
|
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 = {
|
const defaultOptions = {
|
||||||
config: {
|
config: {
|
||||||
|
@ -119,6 +123,26 @@ export const ModActionsPlugin = zeppelinPlugin<ModActionsPluginType>()("mod_acti
|
||||||
UnhideCaseCmd,
|
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) {
|
onLoad(pluginData) {
|
||||||
const { state, guild } = pluginData;
|
const { state, guild } = pluginData;
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { either } from "fp-ts/lib/Either";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { SimpleCache } from "./SimpleCache";
|
import { SimpleCache } from "./SimpleCache";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
import { Awaitable } from "knub/dist/utils";
|
||||||
|
|
||||||
const fsp = fs.promises;
|
const fsp = fs.promises;
|
||||||
|
|
||||||
|
@ -1222,3 +1223,7 @@ export function isFullMessage(msg: PossiblyUncachedMessage): msg is Message {
|
||||||
export function isGuildInvite(invite: AnyInvite): invite is GuildInvite {
|
export function isGuildInvite(invite: AnyInvite): invite is GuildInvite {
|
||||||
return (invite as GuildInvite).guild != null;
|
return (invite as GuildInvite).guild != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function asyncMap<T, R>(arr: T[], fn: (item: T) => Promise<R>): Promise<R[]> {
|
||||||
|
return Promise.all(arr.map((item, index) => fn(item)));
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue