3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-07-07 19:17:19 +00:00

Merge branch 'master' of github.com:ZeppelinBot/Zeppelin into feat/application-commands

This commit is contained in:
Lily Bergonzat 2024-02-16 14:26:34 +01:00
commit 2c0e4b37ca
235 changed files with 3464 additions and 4799 deletions

View file

@ -1,14 +1,13 @@
import { configUtils, CooldownManager } from "knub";
import { CooldownManager } from "knub";
import { Queue } from "../../Queue";
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 { parseIoTsSchema, StrictValidationError } from "../../validatorUtils";
import { CountersPlugin } from "../Counters/CountersPlugin";
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
import { LogsPlugin } from "../Logs/LogsPlugin";
@ -17,13 +16,12 @@ import { MutesPlugin } from "../Mutes/MutesPlugin";
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin";
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 { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger";
import { runAutomodOnMessage } from "./events/runAutomodOnMessage";
import { runAutomodOnModAction } from "./events/runAutomodOnModAction";
import {
@ -35,8 +33,7 @@ import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChang
import { clearOldRecentActions } from "./functions/clearOldRecentActions";
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
import { pluginInfo } from "./info";
import { availableTriggers } from "./triggers/availableTriggers";
import { AutomodPluginType, ConfigSchema } from "./types";
import { AutomodPluginType, zAutomodConfig } from "./types";
const defaultOptions = {
config: {
@ -61,129 +58,6 @@ const defaultOptions = {
],
};
/**
* Config preprocessor to set default values for triggers and perform extra validation
* TODO: Separate input and output types
*/
const configParser = (input: unknown) => {
const rules = (input as any).rules;
if (rules) {
// Loop through each rule
for (const [name, rule] of Object.entries(rules)) {
if (rule == null) {
delete 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;
}
if (rule["affects_self"] == null) {
rule["affects_self"] = 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;
}
if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) {
throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule["name"]}'`]);
}
}
}
}
return parseIoTsSchema(ConfigSchema, input);
};
export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
name: "automod",
showInDocs: true,
@ -201,7 +75,7 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
],
defaultOptions,
configParser,
configParser: (input) => zAutomodConfig.parse(input),
customOverrideCriteriaFunctions: {
antiraid_level: (pluginData, matchParams, value) => {

View file

@ -1,6 +1,6 @@
import { PermissionFlagsBits, Snowflake } from "discord.js";
import * as t from "io-ts";
import { nonNullish, unique } from "../../../utils";
import z from "zod";
import { nonNullish, unique, zSnowflake } from "../../../utils";
import { canAssignRole } from "../../../utils/canAssignRole";
import { getMissingPermissions } from "../../../utils/getMissingPermissions";
import { missingPermissionError } from "../../../utils/missingPermissionError";
@ -11,9 +11,10 @@ import { automodAction } from "../helpers";
const p = PermissionFlagsBits;
const configSchema = z.array(zSnowflake);
export const AddRolesAction = automodAction({
configType: t.array(t.string),
defaultConfig: [],
configSchema,
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const members = unique(contexts.map((c) => c.member).filter(nonNullish));

View file

@ -1,15 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { zBoundedCharacters } from "../../../utils";
import { CountersPlugin } from "../../Counters/CountersPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const AddToCounterAction = automodAction({
configType: t.type({
counter: t.string,
amount: t.number,
}),
const configSchema = z.object({
counter: zBoundedCharacters(0, 100),
amount: z.number(),
});
defaultConfig: {},
export const AddToCounterAction = automodAction({
configSchema,
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const countersPlugin = pluginData.getPlugin(CountersPlugin);

View file

@ -1,6 +1,5 @@
import { Snowflake } from "discord.js";
import * as t from "io-ts";
import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions";
import z from "zod";
import { LogType } from "../../../data/LogType";
import {
createTypedTemplateSafeValueContainer,
@ -12,25 +11,28 @@ import {
chunkMessageLines,
isTruthy,
messageLink,
tAllowedMentions,
tNormalizedNullOptional,
validateAndParseMessageContent,
verboseChannelMention,
zAllowedMentions,
zBoundedCharacters,
zNullishToUndefined,
zSnowflake,
} from "../../../utils";
import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const AlertAction = automodAction({
configType: t.type({
channel: t.string,
text: t.string,
allowed_mentions: tNormalizedNullOptional(tAllowedMentions),
}),
const configSchema = z.object({
channel: zSnowflake,
text: zBoundedCharacters(0, 4000),
allowed_mentions: zNullishToUndefined(zAllowedMentions.nullable().default(null)),
});
defaultConfig: {},
export const AlertAction = automodAction({
configSchema,
async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) {
const channel = pluginData.guild.channels.cache.get(actionConfig.channel as Snowflake);

View file

@ -1,11 +1,12 @@
import { AnyThreadChannel } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { noop } from "../../../utils";
import { automodAction } from "../helpers";
const configSchema = z.strictObject({});
export const ArchiveThreadAction = automodAction({
configType: t.type({}),
defaultConfig: {},
configSchema,
async apply({ pluginData, contexts }) {
const threads = contexts

View file

@ -1,4 +1,3 @@
import * as t from "io-ts";
import { AutomodActionBlueprint } from "../helpers";
import { AddRolesAction } from "./addRoles";
import { AddToCounterAction } from "./addToCounter";
@ -20,7 +19,7 @@ import { SetSlowmodeAction } from "./setSlowmode";
import { StartThreadAction } from "./startThread";
import { WarnAction } from "./warn";
export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
export const availableActions = {
clean: CleanAction,
warn: WarnAction,
mute: MuteAction,
@ -40,26 +39,4 @@ export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
archive_thread: ArchiveThreadAction,
change_perms: ChangePermsAction,
pause_invites: PauseInvitesAction,
};
export const AvailableActions = t.type({
clean: CleanAction.configType,
warn: WarnAction.configType,
mute: MuteAction.configType,
kick: KickAction.configType,
ban: BanAction.configType,
alert: AlertAction.configType,
change_nickname: ChangeNicknameAction.configType,
log: LogAction.configType,
add_roles: AddRolesAction.configType,
remove_roles: RemoveRolesAction.configType,
set_antiraid_level: SetAntiraidLevelAction.configType,
reply: ReplyAction.configType,
add_to_counter: AddToCounterAction.configType,
set_counter: SetCounterAction.configType,
set_slowmode: SetSlowmodeAction.configType,
start_thread: StartThreadAction.configType,
archive_thread: ArchiveThreadAction.configType,
change_perms: ChangePermsAction.configType,
pause_invites: PauseInvitesAction.configType,
});
} satisfies Record<string, AutomodActionBlueprint<any>>;

View file

@ -1,25 +1,30 @@
import * as t from "io-ts";
import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils";
import z from "zod";
import {
convertDelayStringToMS,
nonNullish,
unique,
zBoundedCharacters,
zDelayString,
zSnowflake,
} from "../../../utils";
import { CaseArgs } from "../../Cases/types";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers";
export const BanAction = automodAction({
configType: t.type({
reason: tNullable(t.string),
duration: tNullable(tDelayString),
notify: tNullable(t.string),
notifyChannel: tNullable(t.string),
deleteMessageDays: tNullable(t.number),
postInCaseLog: tNullable(t.boolean),
hide_case: tNullable(t.boolean),
}),
const configSchema = z.strictObject({
reason: zBoundedCharacters(0, 4000).nullable().default(null),
duration: zDelayString.nullable().default(null),
notify: zNotify.nullable().default(null),
notifyChannel: zSnowflake.nullable().default(null),
deleteMessageDays: z.number().nullable().default(null),
postInCaseLog: z.boolean().nullable().default(null),
hide_case: z.boolean().nullable().default(false),
});
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
export const BanAction = automodAction({
configSchema,
async apply({ pluginData, contexts, actionConfig, matchResult }) {
const reason = actionConfig.reason || "Kicked automatically";

View file

@ -1,18 +1,16 @@
import * as t from "io-ts";
import { nonNullish, unique } from "../../../utils";
import z from "zod";
import { nonNullish, unique, zBoundedCharacters } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const ChangeNicknameAction = automodAction({
configType: t.union([
t.string,
t.type({
name: t.string,
configSchema: z.union([
zBoundedCharacters(0, 32),
z.strictObject({
name: zBoundedCharacters(0, 32),
}),
]),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig }) {
const members = unique(contexts.map((c) => c.member).filter(nonNullish));

View file

@ -1,12 +1,14 @@
import { PermissionsBitField, PermissionsString } from "discord.js";
import * as t from "io-ts";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { isValidSnowflake, noop, tNullable, tPartialDictionary } from "../../../utils";
import { U } from "ts-toolbelt";
import z from "zod";
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils";
import {
guildToTemplateSafeGuild,
savedMessageToTemplateSafeSavedMessage,
userToTemplateSafeUser,
} from "../../../utils/templateSafeObjects";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
type LegacyPermMap = Record<string, keyof (typeof PermissionsBitField)["Flags"]>;
@ -59,41 +61,63 @@ const realToLegacyMap = Object.entries(legacyPermMap).reduce((map, pair) => {
return map;
}, {}) as Record<keyof typeof PermissionsBitField.Flags, keyof typeof legacyPermMap>;
export const ChangePermsAction = automodAction({
configType: t.type({
target: t.string,
channel: tNullable(t.string),
perms: tPartialDictionary(
t.union([t.keyof(PermissionsBitField.Flags), t.keyof(legacyPermMap)]),
tNullable(t.boolean),
),
}),
defaultConfig: {},
const permissionNames = keys(PermissionsBitField.Flags) as U.ListOf<keyof typeof PermissionsBitField.Flags>;
const legacyPermissionNames = keys(legacyPermMap) as U.ListOf<keyof typeof legacyPermMap>;
const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as const;
async apply({ pluginData, contexts, actionConfig }) {
export const ChangePermsAction = automodAction({
configSchema: z.strictObject({
target: zBoundedCharacters(1, 2000),
channel: zBoundedCharacters(1, 2000).nullable().default(null),
perms: z.record(z.enum(allPermissionNames), z.boolean().nullable()),
}),
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const user = contexts.find((c) => c.user)?.user;
const message = contexts.find((c) => c.message)?.message;
const renderTarget = async (str: string) =>
renderTemplate(
str,
let target: string;
try {
target = await renderTemplate(
actionConfig.target,
new TemplateSafeValueContainer({
user: user ? userToTemplateSafeUser(user) : null,
guild: guildToTemplateSafeGuild(pluginData.guild),
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
}),
);
const renderChannel = async (str: string) =>
renderTemplate(
str,
new TemplateSafeValueContainer({
user: user ? userToTemplateSafeUser(user) : null,
guild: guildToTemplateSafeGuild(pluginData.guild),
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
}),
);
const target = await renderTarget(actionConfig.target);
const channelId = actionConfig.channel ? await renderChannel(actionConfig.channel) : null;
} catch (err) {
if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in target format of automod rule ${ruleName}: ${err.message}`,
});
return;
}
throw err;
}
let channelId: string | null = null;
if (actionConfig.channel) {
try {
channelId = await renderTemplate(
actionConfig.channel,
new TemplateSafeValueContainer({
user: user ? userToTemplateSafeUser(user) : null,
guild: guildToTemplateSafeGuild(pluginData.guild),
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
}),
);
} catch (err) {
if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in channel format of automod rule ${ruleName}: ${err.message}`,
});
return;
}
throw err;
}
}
const role = pluginData.guild.roles.resolve(target);
if (!role) {
const member = await pluginData.guild.members.fetch(target).catch(noop);

View file

@ -1,12 +1,11 @@
import { GuildTextBasedChannel, Snowflake } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { LogType } from "../../../data/LogType";
import { noop } from "../../../utils";
import { automodAction } from "../helpers";
export const CleanAction = automodAction({
configType: t.boolean,
defaultConfig: false,
configSchema: z.boolean().default(false),
async apply({ pluginData, contexts, ruleName }) {
const messageIdsToDeleteByChannelId: Map<string, string[]> = new Map();

View file

@ -1,13 +1,12 @@
import * as t from "io-ts";
import z from "zod";
import { zBoundedCharacters } from "../../../utils";
import { automodAction } from "../helpers";
export const ExampleAction = automodAction({
configType: t.type({
someValue: t.string,
configSchema: z.strictObject({
someValue: zBoundedCharacters(0, 1000),
}),
defaultConfig: {},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async apply({ pluginData, contexts, actionConfig }) {
// TODO: Everything

View file

@ -1,24 +1,20 @@
import * as t from "io-ts";
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils";
import z from "zod";
import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils";
import { CaseArgs } from "../../Cases/types";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers";
export const KickAction = automodAction({
configType: t.type({
reason: tNullable(t.string),
notify: tNullable(t.string),
notifyChannel: tNullable(t.string),
postInCaseLog: tNullable(t.boolean),
hide_case: tNullable(t.boolean),
configSchema: z.strictObject({
reason: zBoundedCharacters(0, 4000).nullable().default(null),
notify: zNotify.nullable().default(null),
notifyChannel: zSnowflake.nullable().default(null),
postInCaseLog: z.boolean().nullable().default(null),
hide_case: z.boolean().nullable().default(false),
}),
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, matchResult }) {
const reason = actionConfig.reason || "Kicked automatically";
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;

View file

@ -1,11 +1,10 @@
import * as t from "io-ts";
import z from "zod";
import { isTruthy, unique } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const LogAction = automodAction({
configType: t.boolean,
defaultConfig: true,
configSchema: z.boolean().default(true),
async apply({ pluginData, contexts, ruleName, matchResult }) {
const users = unique(contexts.map((c) => c.user)).filter(isTruthy);

View file

@ -1,29 +1,38 @@
import * as t from "io-ts";
import z from "zod";
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils";
import {
convertDelayStringToMS,
nonNullish,
unique,
zBoundedCharacters,
zDelayString,
zSnowflake,
} from "../../../utils";
import { CaseArgs } from "../../Cases/types";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { MutesPlugin } from "../../Mutes/MutesPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers";
export const MuteAction = automodAction({
configType: t.type({
reason: tNullable(t.string),
duration: tNullable(tDelayString),
notify: tNullable(t.string),
notifyChannel: tNullable(t.string),
remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
postInCaseLog: tNullable(t.boolean),
hide_case: tNullable(t.boolean),
configSchema: z.strictObject({
reason: zBoundedCharacters(0, 4000).nullable().default(null),
duration: zDelayString.nullable().default(null),
notify: zNotify.nullable().default(null),
notifyChannel: zSnowflake.nullable().default(null),
remove_roles_on_mute: z
.union([z.boolean(), z.array(zSnowflake)])
.nullable()
.default(null),
restore_roles_on_mute: z
.union([z.boolean(), z.array(zSnowflake)])
.nullable()
.default(null),
postInCaseLog: z.boolean().nullable().default(null),
hide_case: z.boolean().nullable().default(false),
}),
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) {
const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined;
const reason = actionConfig.reason || "Muted automatically";

View file

@ -1,14 +1,12 @@
import { GuildFeature } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { automodAction } from "../helpers";
export const PauseInvitesAction = automodAction({
configType: t.type({
paused: t.boolean,
configSchema: z.strictObject({
paused: z.boolean(),
}),
defaultConfig: {},
async apply({ pluginData, actionConfig }) {
const hasInvitesDisabled = pluginData.guild.features.includes(GuildFeature.InvitesDisabled);

View file

@ -1,6 +1,6 @@
import { PermissionFlagsBits, Snowflake } from "discord.js";
import * as t from "io-ts";
import { nonNullish, unique } from "../../../utils";
import z from "zod";
import { nonNullish, unique, zSnowflake } from "../../../utils";
import { canAssignRole } from "../../../utils/canAssignRole";
import { getMissingPermissions } from "../../../utils/getMissingPermissions";
import { memberRolesLock } from "../../../utils/lockNameHelpers";
@ -12,9 +12,7 @@ import { automodAction } from "../helpers";
const p = PermissionFlagsBits;
export const RemoveRolesAction = automodAction({
configType: t.array(t.string),
defaultConfig: [],
configSchema: z.array(zSnowflake).default([]),
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const members = unique(contexts.map((c) => c.member).filter(nonNullish));

View file

@ -1,16 +1,16 @@
import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js";
import * as t from "io-ts";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import z from "zod";
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import {
convertDelayStringToMS,
noop,
renderRecursively,
tDelayString,
tMessageContent,
tNullable,
unique,
validateAndParseMessageContent,
verboseChannelMention,
zBoundedCharacters,
zDelayString,
zMessageContent,
} from "../../../utils";
import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
@ -20,17 +20,15 @@ import { automodAction } from "../helpers";
import { AutomodContext } from "../types";
export const ReplyAction = automodAction({
configType: t.union([
t.string,
t.type({
text: tMessageContent,
auto_delete: tNullable(t.union([tDelayString, t.number])),
inline: tNullable(t.boolean),
configSchema: z.union([
zBoundedCharacters(0, 4000),
z.strictObject({
text: zMessageContent,
auto_delete: z.union([zDelayString, z.number()]).nullable().default(null),
inline: z.boolean().default(false),
}),
]),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const contextsWithTextChannels = contexts
.filter((c) => c.message?.channel_id)
@ -60,10 +58,21 @@ export const ReplyAction = automodAction({
}),
);
const formatted =
typeof actionConfig === "string"
? await renderReplyText(actionConfig)
: ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions);
let formatted: string | MessageCreateOptions;
try {
formatted =
typeof actionConfig === "string"
? await renderReplyText(actionConfig)
: ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions);
} catch (err) {
if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in reply format of automod rule \`${ruleName}\`: ${err.message}`,
});
return;
}
throw err;
}
if (formatted) {
const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as GuildTextBasedChannel;

View file

@ -1,11 +1,9 @@
import * as t from "io-ts";
import { tNullable } from "../../../utils";
import { zBoundedCharacters } from "../../../utils";
import { setAntiraidLevel } from "../functions/setAntiraidLevel";
import { automodAction } from "../helpers";
export const SetAntiraidLevelAction = automodAction({
configType: tNullable(t.string),
defaultConfig: "",
configSchema: zBoundedCharacters(0, 100).nullable(),
async apply({ pluginData, actionConfig }) {
setAntiraidLevel(pluginData, actionConfig ?? null);

View file

@ -1,16 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../../data/GuildCounters";
import { zBoundedCharacters } from "../../../utils";
import { CountersPlugin } from "../../Counters/CountersPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const SetCounterAction = automodAction({
configType: t.type({
counter: t.string,
value: t.number,
configSchema: z.strictObject({
counter: zBoundedCharacters(0, 100),
value: z.number().min(MIN_COUNTER_VALUE).max(MAX_COUNTER_VALUE),
}),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const countersPlugin = pluginData.getPlugin(CountersPlugin);
if (!countersPlugin.counterExists(actionConfig.counter)) {

View file

@ -1,19 +1,15 @@
import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js";
import * as t from "io-ts";
import { convertDelayStringToMS, isDiscordAPIError, tDelayString, tNullable } from "../../../utils";
import z from "zod";
import { convertDelayStringToMS, isDiscordAPIError, zDelayString, zSnowflake } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const SetSlowmodeAction = automodAction({
configType: t.type({
channels: tNullable(t.array(t.string)),
duration: tNullable(tDelayString),
configSchema: z.strictObject({
channels: z.array(zSnowflake),
duration: zDelayString.nullable().default("10s"),
}),
defaultConfig: {
duration: "10s",
},
async apply({ pluginData, actionConfig, contexts }) {
const slowmodeMs = Math.max(actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : 0, 0);
const channels: Snowflake[] = actionConfig.channels ?? [];

View file

@ -1,14 +1,9 @@
import {
ChannelType,
GuildFeature,
GuildTextThreadCreateOptions,
ThreadAutoArchiveDuration,
ThreadChannel,
} from "discord.js";
import * as t from "io-ts";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { MINUTES, convertDelayStringToMS, noop, tDelayString, tNullable } from "../../../utils";
import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js";
import z from "zod";
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils";
import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [
@ -19,19 +14,15 @@ const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [
];
export const StartThreadAction = automodAction({
configType: t.type({
name: tNullable(t.string),
auto_archive: tDelayString,
private: tNullable(t.boolean),
slowmode: tNullable(tDelayString),
limit_per_channel: tNullable(t.number),
configSchema: z.strictObject({
name: zBoundedCharacters(1, 100).nullable(),
auto_archive: zDelayString,
private: z.boolean().default(false),
slowmode: zDelayString.nullable().default(null),
limit_per_channel: z.number().nullable().default(5),
}),
defaultConfig: {
limit_per_channel: 5,
},
async apply({ pluginData, contexts, actionConfig }) {
async apply({ pluginData, contexts, actionConfig, ruleName }) {
// check if the message still exists, we don't want to create threads for deleted messages
const threads = contexts.filter((c) => {
if (!c.message || !c.user) return false;
@ -47,7 +38,6 @@ export const StartThreadAction = automodAction({
return true;
});
const guild = pluginData.guild;
const archiveSet = actionConfig.auto_archive
? Math.ceil(Math.max(convertDelayStringToMS(actionConfig.auto_archive) ?? 0, 0) / MINUTES)
: ThreadAutoArchiveDuration.OneDay;
@ -57,24 +47,31 @@ export const StartThreadAction = automodAction({
for (const threadContext of threads) {
const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id);
if (!channel || !("threads" in channel) || channel.type === ChannelType.GuildForum) continue;
if (!channel || !("threads" in channel) || channel.isThreadOnly()) continue;
const renderThreadName = async (str: string) =>
renderTemplate(
str,
let threadName: string;
try {
threadName = await renderTemplate(
actionConfig.name ?? "{user.renderedUsername}'s thread",
new TemplateSafeValueContainer({
user: userToTemplateSafeUser(threadContext.user!),
msg: savedMessageToTemplateSafeSavedMessage(threadContext.message!),
}),
);
const threadName = await renderThreadName(actionConfig.name ?? "{user.renderedUsername}'s thread");
} catch (err) {
if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in thread name format of automod rule ${ruleName}: ${err.message}`,
});
return;
}
throw err;
}
const threadOptions: GuildTextThreadCreateOptions<unknown> = {
name: threadName,
autoArchiveDuration: autoArchive,
startMessage:
!actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads)
? threadContext.message!.id
: undefined,
startMessage: !actionConfig.private ? threadContext.message!.id : undefined,
};
let thread: ThreadChannel | undefined;
@ -90,10 +87,7 @@ export const StartThreadAction = automodAction({
.create({
...threadOptions,
type: actionConfig.private ? ChannelType.PrivateThread : ChannelType.PublicThread,
startMessage:
!actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads)
? threadContext.message!.id
: undefined,
startMessage: !actionConfig.private ? threadContext.message!.id : undefined,
})
.catch(() => undefined);
}

View file

@ -1,24 +1,20 @@
import * as t from "io-ts";
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils";
import z from "zod";
import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils";
import { CaseArgs } from "../../Cases/types";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers";
export const WarnAction = automodAction({
configType: t.type({
reason: tNullable(t.string),
notify: tNullable(t.string),
notifyChannel: tNullable(t.string),
postInCaseLog: tNullable(t.boolean),
hide_case: tNullable(t.boolean),
configSchema: z.strictObject({
reason: zBoundedCharacters(0, 4000).nullable().default(null),
notify: zNotify.nullable().default(null),
notifyChannel: zSnowflake.nullable().default(null),
postInCaseLog: z.boolean().nullable().default(null),
hide_case: z.boolean().nullable().default(false),
}),
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, matchResult }) {
const reason = actionConfig.reason || "Warned automatically";
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;

View file

@ -1,3 +1,4 @@
import z from "zod";
import { MINUTES, SECONDS } from "../../utils";
export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
@ -18,3 +19,5 @@ export enum RecentActionType {
MemberLeave,
ThreadCreate,
}
export const zNotify = z.union([z.literal("dm"), z.literal("channel")]);

View file

@ -1,28 +1,27 @@
import * as t from "io-ts";
import z from "zod";
import { SavedMessage } from "../../../data/entities/SavedMessage";
import { humanizeDurationShort } from "../../../humanizeDurationShort";
import { getBaseUrl } from "../../../pluginUtils";
import { convertDelayStringToMS, sorter, tDelayString, tNullable } from "../../../utils";
import { convertDelayStringToMS, sorter, zDelayString } from "../../../utils";
import { RecentActionType } from "../constants";
import { automodTrigger } from "../helpers";
import { findRecentSpam } from "./findRecentSpam";
import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions";
import { getMessageSpamIdentifier } from "./getSpamIdentifier";
const MessageSpamTriggerConfig = t.type({
amount: t.number,
within: tDelayString,
per_channel: tNullable(t.boolean),
});
interface TMessageSpamMatchResultType {
archiveId: string;
}
const configSchema = z.strictObject({
amount: z.number().int(),
within: zDelayString,
per_channel: z.boolean().nullable().default(false),
});
export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: string) {
return automodTrigger<TMessageSpamMatchResultType>()({
configType: MessageSpamTriggerConfig,
defaultConfig: {},
configSchema,
async match({ pluginData, context, triggerConfig }) {
if (!context.message) {

View file

@ -42,7 +42,7 @@ export async function* matchMultipleTextTypesOnMessage(
}
if (trigger.match_visible_names) {
yield ["visiblename", member.nickname || msg.data.author.username];
yield ["visiblename", member.displayName || msg.data.author.username];
}
if (trigger.match_usernames) {

View file

@ -1,5 +1,5 @@
import * as t from "io-ts";
import { GuildPluginData } from "knub";
import z, { ZodTypeAny } from "zod";
import { Awaitable } from "../../utils/typeUtils";
import { AutomodContext, AutomodPluginType } from "./types";
@ -31,21 +31,19 @@ type AutomodTriggerRenderMatchInformationFn<TConfigType, TMatchResultExtra> = (m
matchResult: AutomodTriggerMatchResult<TMatchResultExtra>;
}) => Awaitable<string>;
export interface AutomodTriggerBlueprint<TConfigType extends t.Any, TMatchResultExtra> {
configType: TConfigType;
defaultConfig: Partial<t.TypeOf<TConfigType>>;
match: AutomodTriggerMatchFn<t.TypeOf<TConfigType>, TMatchResultExtra>;
renderMatchInformation: AutomodTriggerRenderMatchInformationFn<t.TypeOf<TConfigType>, TMatchResultExtra>;
export interface AutomodTriggerBlueprint<TConfigSchema extends ZodTypeAny, TMatchResultExtra> {
configSchema: TConfigSchema;
match: AutomodTriggerMatchFn<z.output<TConfigSchema>, TMatchResultExtra>;
renderMatchInformation: AutomodTriggerRenderMatchInformationFn<z.output<TConfigSchema>, TMatchResultExtra>;
}
export function automodTrigger<TMatchResultExtra>(): <TConfigType extends t.Any>(
blueprint: AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>,
) => AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>;
export function automodTrigger<TMatchResultExtra>(): <TConfigSchema extends ZodTypeAny>(
blueprint: AutomodTriggerBlueprint<TConfigSchema, TMatchResultExtra>,
) => AutomodTriggerBlueprint<TConfigSchema, TMatchResultExtra>;
export function automodTrigger<TConfigType extends t.Any>(
blueprint: AutomodTriggerBlueprint<TConfigType, unknown>,
): AutomodTriggerBlueprint<TConfigType, unknown>;
export function automodTrigger<TConfigSchema extends ZodTypeAny>(
blueprint: AutomodTriggerBlueprint<TConfigSchema, unknown>,
): AutomodTriggerBlueprint<TConfigSchema, unknown>;
export function automodTrigger(...args) {
if (args.length) {
@ -63,15 +61,13 @@ type AutomodActionApplyFn<TConfigType> = (meta: {
matchResult: AutomodTriggerMatchResult;
}) => Awaitable<void>;
export interface AutomodActionBlueprint<TConfigType extends t.Any> {
configType: TConfigType;
defaultConfig: Partial<t.TypeOf<TConfigType>>;
apply: AutomodActionApplyFn<t.TypeOf<TConfigType>>;
export interface AutomodActionBlueprint<TConfigSchema extends ZodTypeAny> {
configSchema: TConfigSchema;
apply: AutomodActionApplyFn<z.output<TConfigSchema>>;
}
export function automodAction<TConfigType extends t.Any>(
blueprint: AutomodActionBlueprint<TConfigType>,
): AutomodActionBlueprint<TConfigType> {
export function automodAction<TConfigSchema extends ZodTypeAny>(
blueprint: AutomodActionBlueprint<TConfigSchema>,
): AutomodActionBlueprint<TConfigSchema> {
return blueprint;
}

View file

@ -1,6 +1,6 @@
import { trimPluginDescription } from "../../utils";
import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint";
import { ConfigSchema } from "./types";
import { zAutomodConfig } from "./types";
export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
prettyName: "Automod",
@ -100,5 +100,5 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
{matchSummary}
~~~
`),
configSchema: ConfigSchema,
configSchema: zAutomodConfig,
};

View file

@ -1,16 +1,15 @@
import * as t from "io-ts";
import { tNullable } from "../../../utils";
import z from "zod";
import { automodTrigger } from "../helpers";
interface AntiraidLevelTriggerResult {}
export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({
configType: t.type({
level: tNullable(t.string),
only_on_change: tNullable(t.boolean),
}),
const configSchema = z.strictObject({
level: z.nullable(z.string().max(100)),
only_on_change: z.nullable(z.boolean()),
});
defaultConfig: {},
export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({
configSchema,
async match({ triggerConfig, context }) {
if (!context.antiraid) {

View file

@ -1,14 +1,14 @@
import { Snowflake } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers";
interface AnyMessageResultType {}
export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({
configType: t.type({}),
const configSchema = z.strictObject({});
defaultConfig: {},
export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({
configSchema,
async match({ context }) {
if (!context.message) {

View file

@ -1,4 +1,3 @@
import * as t from "io-ts";
import { AutomodTriggerBlueprint } from "../helpers";
import { AntiraidLevelTrigger } from "./antiraidLevel";
import { AnyMessageTrigger } from "./anyMessage";
@ -45,6 +44,7 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
match_attachment_type: MatchAttachmentTypeTrigger,
match_mime_type: MatchMimeTypeTrigger,
member_join: MemberJoinTrigger,
member_leave: MemberLeaveTrigger,
role_added: RoleAddedTrigger,
role_removed: RoleRemovedTrigger,
@ -76,46 +76,3 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
thread_archive: ThreadArchiveTrigger,
thread_unarchive: ThreadUnarchiveTrigger,
};
export const AvailableTriggers = t.type({
any_message: AnyMessageTrigger.configType,
match_words: MatchWordsTrigger.configType,
match_regex: MatchRegexTrigger.configType,
match_invites: MatchInvitesTrigger.configType,
match_links: MatchLinksTrigger.configType,
match_attachment_type: MatchAttachmentTypeTrigger.configType,
match_mime_type: MatchMimeTypeTrigger.configType,
member_join: MemberJoinTrigger.configType,
member_leave: MemberLeaveTrigger.configType,
role_added: RoleAddedTrigger.configType,
role_removed: RoleRemovedTrigger.configType,
message_spam: MessageSpamTrigger.configType,
mention_spam: MentionSpamTrigger.configType,
link_spam: LinkSpamTrigger.configType,
attachment_spam: AttachmentSpamTrigger.configType,
emoji_spam: EmojiSpamTrigger.configType,
line_spam: LineSpamTrigger.configType,
character_spam: CharacterSpamTrigger.configType,
member_join_spam: MemberJoinSpamTrigger.configType,
sticker_spam: StickerSpamTrigger.configType,
thread_create_spam: ThreadCreateSpamTrigger.configType,
counter_trigger: CounterTrigger.configType,
note: NoteTrigger.configType,
warn: WarnTrigger.configType,
mute: MuteTrigger.configType,
unmute: UnmuteTrigger.configType,
kick: KickTrigger.configType,
ban: BanTrigger.configType,
unban: UnbanTrigger.configType,
antiraid_level: AntiraidLevelTrigger.configType,
thread_create: ThreadCreateTrigger.configType,
thread_delete: ThreadDeleteTrigger.configType,
thread_archive: ThreadArchiveTrigger.configType,
thread_unarchive: ThreadUnarchiveTrigger.configType,
});

View file

@ -1,19 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface BanTriggerResultType {}
export const BanTrigger = automodTrigger<BanTriggerResultType>()({
configType: t.type({
manual: t.boolean,
automatic: t.boolean,
}),
const configSchema = z.strictObject({
manual: z.boolean().default(true),
automatic: z.boolean().default(true),
});
defaultConfig: {
manual: true,
automatic: true,
},
export const BanTrigger = automodTrigger<BanTriggerResultType>()({
configSchema,
async match({ context, triggerConfig }) {
if (context.modAction?.type !== "ban") {

View file

@ -1,18 +1,17 @@
import * as t from "io-ts";
import { tNullable } from "../../../utils";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line
interface CounterTriggerResult {}
export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
configType: t.type({
counter: t.string,
trigger: t.string,
reverse: tNullable(t.boolean),
}),
const configSchema = z.strictObject({
counter: z.string().max(100),
trigger: z.string().max(100),
reverse: z.boolean().optional(),
});
defaultConfig: {},
export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
configSchema,
async match({ triggerConfig, context }) {
if (!context.counterTrigger) {

View file

@ -1,18 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
interface ExampleMatchResultType {
isBanana: boolean;
}
export const ExampleTrigger = automodTrigger<ExampleMatchResultType>()({
configType: t.type({
allowedFruits: t.array(t.string),
}),
const configSchema = z.strictObject({
allowedFruits: z.array(z.string().max(100)).max(50).default(["peach", "banana"]),
});
defaultConfig: {
allowedFruits: ["peach", "banana"],
},
export const ExampleTrigger = automodTrigger<ExampleMatchResultType>()({
configSchema,
async match({ triggerConfig, context }) {
const foundFruit = triggerConfig.allowedFruits.find((fruit) => context.message?.data.content === fruit);

View file

@ -1,19 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface KickTriggerResultType {}
export const KickTrigger = automodTrigger<KickTriggerResultType>()({
configType: t.type({
manual: t.boolean,
automatic: t.boolean,
}),
const configSchema = z.strictObject({
manual: z.boolean().default(true),
automatic: z.boolean().default(true),
});
defaultConfig: {
manual: true,
automatic: true,
},
export const KickTrigger = automodTrigger<KickTriggerResultType>()({
configSchema,
async match({ context, triggerConfig }) {
if (context.modAction?.type !== "kick") {

View file

@ -1,6 +1,6 @@
import { escapeInlineCode, Snowflake } from "discord.js";
import * as t from "io-ts";
import { extname } from "path";
import z from "zod";
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers";
@ -9,20 +9,33 @@ interface MatchResultType {
mode: "blacklist" | "whitelist";
}
export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
configType: t.type({
filetype_blacklist: t.array(t.string),
blacklist_enabled: t.boolean,
filetype_whitelist: t.array(t.string),
whitelist_enabled: t.boolean,
}),
const configSchema = z
.strictObject({
filetype_blacklist: z.array(z.string().max(32)).max(255).default([]),
blacklist_enabled: z.boolean().default(false),
filetype_whitelist: z.array(z.string().max(32)).max(255).default([]),
whitelist_enabled: z.boolean().default(false),
})
.transform((parsed, ctx) => {
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Cannot have both blacklist and whitelist enabled",
});
return z.NEVER;
}
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Must have either blacklist or whitelist enabled",
});
return z.NEVER;
}
return parsed;
});
defaultConfig: {
filetype_blacklist: [],
blacklist_enabled: false,
filetype_whitelist: [],
whitelist_enabled: false,
},
export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
configSchema,
async match({ context, triggerConfig: trigger }) {
if (!context.message) {

View file

@ -1,5 +1,5 @@
import * as t from "io-ts";
import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, tNullable } from "../../../utils";
import z from "zod";
import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, zSnowflake } from "../../../utils";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
import { automodTrigger } from "../helpers";
@ -10,30 +10,22 @@ interface MatchResultType {
invite?: GuildInvite;
}
export const MatchInvitesTrigger = automodTrigger<MatchResultType>()({
configType: t.type({
include_guilds: tNullable(t.array(t.string)),
exclude_guilds: tNullable(t.array(t.string)),
include_invite_codes: tNullable(t.array(t.string)),
exclude_invite_codes: tNullable(t.array(t.string)),
allow_group_dm_invites: t.boolean,
match_messages: t.boolean,
match_embeds: t.boolean,
match_visible_names: t.boolean,
match_usernames: t.boolean,
match_nicknames: t.boolean,
match_custom_status: t.boolean,
}),
const configSchema = z.strictObject({
include_guilds: z.array(zSnowflake).max(255).optional(),
exclude_guilds: z.array(zSnowflake).max(255).optional(),
include_invite_codes: z.array(z.string().max(32)).max(255).optional(),
exclude_invite_codes: z.array(z.string().max(32)).max(255).optional(),
allow_group_dm_invites: z.boolean().default(false),
match_messages: z.boolean().default(true),
match_embeds: z.boolean().default(false),
match_visible_names: z.boolean().default(false),
match_usernames: z.boolean().default(false),
match_nicknames: z.boolean().default(false),
match_custom_status: z.boolean().default(false),
});
defaultConfig: {
allow_group_dm_invites: false,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
},
export const MatchInvitesTrigger = automodTrigger<MatchResultType>()({
configSchema,
async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) {

View file

@ -1,11 +1,10 @@
import { escapeInlineCode } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { allowTimeout } from "../../../RegExpRunner";
import { phishermanDomainIsSafe } from "../../../data/Phisherman";
import { getUrlsInString, tNullable } from "../../../utils";
import { getUrlsInString, zRegex } from "../../../utils";
import { mergeRegexes } from "../../../utils/mergeRegexes";
import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex";
import { TRegex } from "../../../validatorUtils";
import { PhishermanPlugin } from "../../Phisherman/PhishermanPlugin";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
@ -21,40 +20,37 @@ const regexCache = new WeakMap<any, RegExp[]>();
const quickLinkCheck = /^https?:\/\//i;
export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
configType: t.type({
include_domains: tNullable(t.array(t.string)),
exclude_domains: tNullable(t.array(t.string)),
include_subdomains: t.boolean,
include_words: tNullable(t.array(t.string)),
exclude_words: tNullable(t.array(t.string)),
include_regex: tNullable(t.array(TRegex)),
exclude_regex: tNullable(t.array(TRegex)),
phisherman: tNullable(
t.type({
include_suspected: tNullable(t.boolean),
include_verified: tNullable(t.boolean),
}),
),
only_real_links: t.boolean,
match_messages: t.boolean,
match_embeds: t.boolean,
match_visible_names: t.boolean,
match_usernames: t.boolean,
match_nicknames: t.boolean,
match_custom_status: t.boolean,
}),
const configSchema = z.strictObject({
include_domains: z.array(z.string().max(255)).max(700).optional(),
exclude_domains: z.array(z.string().max(255)).max(700).optional(),
include_subdomains: z.boolean().default(true),
include_words: z.array(z.string().max(2000)).max(700).optional(),
exclude_words: z.array(z.string().max(2000)).max(700).optional(),
include_regex: z
.array(zRegex(z.string().max(2000)))
.max(512)
.optional(),
exclude_regex: z
.array(zRegex(z.string().max(2000)))
.max(512)
.optional(),
phisherman: z
.strictObject({
include_suspected: z.boolean().optional(),
include_verified: z.boolean().optional(),
})
.optional(),
only_real_links: z.boolean().default(true),
match_messages: z.boolean().default(true),
match_embeds: z.boolean().default(true),
match_visible_names: z.boolean().default(false),
match_usernames: z.boolean().default(false),
match_nicknames: z.boolean().default(false),
match_custom_status: z.boolean().default(false),
});
defaultConfig: {
include_subdomains: true,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
only_real_links: true,
},
export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
configSchema,
async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) {

View file

@ -1,5 +1,5 @@
import { escapeInlineCode } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers";
@ -8,20 +8,33 @@ interface MatchResultType {
mode: "blacklist" | "whitelist";
}
export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({
configType: t.type({
mime_type_blacklist: t.array(t.string),
blacklist_enabled: t.boolean,
mime_type_whitelist: t.array(t.string),
whitelist_enabled: t.boolean,
}),
const configSchema = z
.strictObject({
mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]),
blacklist_enabled: z.boolean().default(false),
mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]),
whitelist_enabled: z.boolean().default(false),
})
.transform((parsed, ctx) => {
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Cannot have both blacklist and whitelist enabled",
});
return z.NEVER;
}
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Must have either blacklist or whitelist enabled",
});
return z.NEVER;
}
return parsed;
});
defaultConfig: {
mime_type_blacklist: [],
blacklist_enabled: false,
mime_type_whitelist: [],
whitelist_enabled: false,
},
export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({
configSchema,
async match({ context, triggerConfig: trigger }) {
if (!context.message) return;

View file

@ -1,9 +1,9 @@
import * as t from "io-ts";
import z from "zod";
import { allowTimeout } from "../../../RegExpRunner";
import { zRegex } from "../../../utils";
import { mergeRegexes } from "../../../utils/mergeRegexes";
import { normalizeText } from "../../../utils/normalizeText";
import { stripMarkdown } from "../../../utils/stripMarkdown";
import { TRegex } from "../../../validatorUtils";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
import { automodTrigger } from "../helpers";
@ -13,33 +13,23 @@ interface MatchResultType {
type: MatchableTextType;
}
const configSchema = z.strictObject({
patterns: z.array(zRegex(z.string().max(2000))).max(512),
case_sensitive: z.boolean().default(false),
normalize: z.boolean().default(false),
strip_markdown: z.boolean().default(false),
match_messages: z.boolean().default(true),
match_embeds: z.boolean().default(false),
match_visible_names: z.boolean().default(false),
match_usernames: z.boolean().default(false),
match_nicknames: z.boolean().default(false),
match_custom_status: z.boolean().default(false),
});
const regexCache = new WeakMap<any, RegExp[]>();
export const MatchRegexTrigger = automodTrigger<MatchResultType>()({
configType: t.type({
patterns: t.array(TRegex),
case_sensitive: t.boolean,
normalize: t.boolean,
strip_markdown: t.boolean,
match_messages: t.boolean,
match_embeds: t.boolean,
match_visible_names: t.boolean,
match_usernames: t.boolean,
match_nicknames: t.boolean,
match_custom_status: t.boolean,
}),
defaultConfig: {
case_sensitive: false,
normalize: false,
strip_markdown: false,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
},
configSchema,
async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) {

View file

@ -1,5 +1,5 @@
import escapeStringRegexp from "escape-string-regexp";
import * as t from "io-ts";
import z from "zod";
import { normalizeText } from "../../../utils/normalizeText";
import { stripMarkdown } from "../../../utils/stripMarkdown";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
@ -13,37 +13,24 @@ interface MatchResultType {
const regexCache = new WeakMap<any, RegExp[]>();
export const MatchWordsTrigger = automodTrigger<MatchResultType>()({
configType: t.type({
words: t.array(t.string),
case_sensitive: t.boolean,
only_full_words: t.boolean,
normalize: t.boolean,
loose_matching: t.boolean,
loose_matching_threshold: t.number,
strip_markdown: t.boolean,
match_messages: t.boolean,
match_embeds: t.boolean,
match_visible_names: t.boolean,
match_usernames: t.boolean,
match_nicknames: t.boolean,
match_custom_status: t.boolean,
}),
const configSchema = z.strictObject({
words: z.array(z.string().max(2000)).max(1024),
case_sensitive: z.boolean().default(false),
only_full_words: z.boolean().default(true),
normalize: z.boolean().default(false),
loose_matching: z.boolean().default(false),
loose_matching_threshold: z.number().int().default(4),
strip_markdown: z.boolean().default(false),
match_messages: z.boolean().default(true),
match_embeds: z.boolean().default(false),
match_visible_names: z.boolean().default(false),
match_usernames: z.boolean().default(false),
match_nicknames: z.boolean().default(false),
match_custom_status: z.boolean().default(false),
});
defaultConfig: {
case_sensitive: false,
only_full_words: true,
normalize: false,
loose_matching: false,
loose_matching_threshold: 4,
strip_markdown: false,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
},
export const MatchWordsTrigger = automodTrigger<MatchResultType>()({
configSchema,
async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) {

View file

@ -1,17 +1,14 @@
import * as t from "io-ts";
import { convertDelayStringToMS, tDelayString } from "../../../utils";
import z from "zod";
import { convertDelayStringToMS, zDelayString } from "../../../utils";
import { automodTrigger } from "../helpers";
export const MemberJoinTrigger = automodTrigger<unknown>()({
configType: t.type({
only_new: t.boolean,
new_threshold: tDelayString,
}),
const configSchema = z.strictObject({
only_new: z.boolean().default(false),
new_threshold: zDelayString.default("1h"),
});
defaultConfig: {
only_new: false,
new_threshold: "1h",
},
export const MemberJoinTrigger = automodTrigger<unknown>()({
configSchema,
async match({ context, triggerConfig }) {
if (!context.joined || !context.member) {

View file

@ -1,18 +1,18 @@
import * as t from "io-ts";
import { convertDelayStringToMS, tDelayString } from "../../../utils";
import z from "zod";
import { convertDelayStringToMS, zDelayString } from "../../../utils";
import { RecentActionType } from "../constants";
import { findRecentSpam } from "../functions/findRecentSpam";
import { getMatchingRecentActions } from "../functions/getMatchingRecentActions";
import { sumRecentActionCounts } from "../functions/sumRecentActionCounts";
import { automodTrigger } from "../helpers";
export const MemberJoinSpamTrigger = automodTrigger<unknown>()({
configType: t.type({
amount: t.number,
within: tDelayString,
}),
const configSchema = z.strictObject({
amount: z.number().int(),
within: zDelayString,
});
defaultConfig: {},
export const MemberJoinSpamTrigger = automodTrigger<unknown>()({
configSchema,
async match({ pluginData, context, triggerConfig }) {
if (!context.joined || !context.member) {

View file

@ -1,10 +1,10 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
export const MemberLeaveTrigger = automodTrigger<unknown>()({
configType: t.type({}),
const configSchema = z.strictObject({});
defaultConfig: {},
export const MemberLeaveTrigger = automodTrigger<unknown>()({
configSchema,
async match({ context }) {
if (!context.joined || !context.member) {

View file

@ -1,19 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface MuteTriggerResultType {}
export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({
configType: t.type({
manual: t.boolean,
automatic: t.boolean,
}),
const configSchema = z.strictObject({
manual: z.boolean().default(true),
automatic: z.boolean().default(true),
});
defaultConfig: {
manual: true,
automatic: true,
},
export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({
configSchema,
async match({ context, triggerConfig }) {
if (context.modAction?.type !== "mute") {

View file

@ -1,12 +1,13 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface NoteTriggerResultType {}
const configSchema = z.strictObject({});
export const NoteTrigger = automodTrigger<NoteTriggerResultType>()({
configType: t.type({}),
defaultConfig: {},
configSchema,
async match({ context }) {
if (context.modAction?.type !== "note") {

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js";
import * as t from "io-ts";
import { renderUserUsername } from "../../../utils";
import z from "zod";
import { renderUsername, zSnowflake } from "../../../utils";
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges";
import { automodTrigger } from "../helpers";
@ -8,10 +8,10 @@ interface RoleAddedMatchResult {
matchedRoleId: string;
}
export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({
configType: t.union([t.string, t.array(t.string)]),
const configSchema = z.union([zSnowflake, z.array(zSnowflake).max(255)]).default([]);
defaultConfig: "",
export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({
configSchema,
async match({ triggerConfig, context, pluginData }) {
if (!context.member || !context.rolesChanged || context.rolesChanged.added!.length === 0) {
@ -38,7 +38,7 @@ export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({
const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake);
const roleName = role?.name || "Unknown";
const member = contexts[0].member!;
const memberName = `**${renderUserUsername(member.user)}** (\`${member.id}\`)`;
const memberName = `**${renderUsername(member)}** (\`${member.id}\`)`;
return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was added to ${memberName}`;
},
});

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js";
import * as t from "io-ts";
import { renderUserUsername } from "../../../utils";
import z from "zod";
import { renderUsername, zSnowflake } from "../../../utils";
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges";
import { automodTrigger } from "../helpers";
@ -8,10 +8,10 @@ interface RoleAddedMatchResult {
matchedRoleId: string;
}
export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({
configType: t.union([t.string, t.array(t.string)]),
const configSchema = z.union([zSnowflake, z.array(zSnowflake).max(255)]).default([]);
defaultConfig: "",
export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({
configSchema,
async match({ triggerConfig, context, pluginData }) {
if (!context.member || !context.rolesChanged || context.rolesChanged.removed!.length === 0) {
@ -38,7 +38,7 @@ export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({
const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake);
const roleName = role?.name || "Unknown";
const member = contexts[0].member!;
const memberName = `**${renderUserUsername(member.user)}** (\`${member.id}\`)`;
const memberName = `**${renderUsername(member)}** (\`${member.id}\`)`;
return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was removed from ${memberName}`;
},
});

View file

@ -1,6 +1,6 @@
import { User, escapeBold, type Snowflake } from "discord.js";
import * as t from "io-ts";
import { tNullable } from "../../../utils";
import z from "zod";
import { renderUsername } from "../../../utils";
import { automodTrigger } from "../helpers";
interface ThreadArchiveResult {
@ -11,12 +11,12 @@ interface ThreadArchiveResult {
matchedThreadOwner: User | undefined;
}
export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({
configType: t.type({
locked: tNullable(t.boolean),
}),
const configSchema = z.strictObject({
locked: z.boolean().optional(),
});
defaultConfig: {},
export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({
configSchema,
async match({ context, triggerConfig }) {
if (!context.threadChange?.archived) {
@ -48,7 +48,7 @@ export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({
const parentName = matchResult.extra.matchedThreadParentName;
const base = `Thread **#${threadName}** (\`${threadId}\`) has been archived in the **#${parentName}** (\`${parentId}\`) channel`;
if (threadOwner) {
return `${base} by **${escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`;
return `${base} by **${escapeBold(renderUsername(threadOwner))}** (\`${threadOwner.id}\`)`;
}
return base;
},

View file

@ -1,5 +1,6 @@
import { User, escapeBold, type Snowflake } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { renderUsername } from "../../../utils.js";
import { automodTrigger } from "../helpers";
interface ThreadCreateResult {
@ -10,9 +11,10 @@ interface ThreadCreateResult {
matchedThreadOwner: User | undefined;
}
const configSchema = z.strictObject({});
export const ThreadCreateTrigger = automodTrigger<ThreadCreateResult>()({
configType: t.type({}),
defaultConfig: {},
configSchema,
async match({ context }) {
if (!context.threadChange?.created) {
@ -40,7 +42,7 @@ export const ThreadCreateTrigger = automodTrigger<ThreadCreateResult>()({
const parentName = matchResult.extra.matchedThreadParentName;
const base = `Thread **#${threadName}** (\`${threadId}\`) has been created in the **#${parentName}** (\`${parentId}\`) channel`;
if (threadOwner) {
return `${base} by **${escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`;
return `${base} by **${escapeBold(renderUsername(threadOwner))}** (\`${threadOwner.id}\`)`;
}
return base;
},

View file

@ -1,18 +1,18 @@
import * as t from "io-ts";
import { convertDelayStringToMS, tDelayString } from "../../../utils";
import z from "zod";
import { convertDelayStringToMS, zDelayString } from "../../../utils";
import { RecentActionType } from "../constants";
import { findRecentSpam } from "../functions/findRecentSpam";
import { getMatchingRecentActions } from "../functions/getMatchingRecentActions";
import { sumRecentActionCounts } from "../functions/sumRecentActionCounts";
import { automodTrigger } from "../helpers";
export const ThreadCreateSpamTrigger = automodTrigger<unknown>()({
configType: t.type({
amount: t.number,
within: tDelayString,
}),
const configSchema = z.strictObject({
amount: z.number().int(),
within: zDelayString,
});
defaultConfig: {},
export const ThreadCreateSpamTrigger = automodTrigger<unknown>()({
configSchema,
async match({ pluginData, context, triggerConfig }) {
if (!context.threadChange?.created) {

View file

@ -1,5 +1,6 @@
import { User, escapeBold, type Snowflake } from "discord.js";
import * as t from "io-ts";
import z from "zod";
import { renderUsername } from "../../../utils.js";
import { automodTrigger } from "../helpers";
interface ThreadDeleteResult {
@ -10,9 +11,10 @@ interface ThreadDeleteResult {
matchedThreadOwner: User | undefined;
}
const configSchema = z.strictObject({});
export const ThreadDeleteTrigger = automodTrigger<ThreadDeleteResult>()({
configType: t.type({}),
defaultConfig: {},
configSchema,
async match({ context }) {
if (!context.threadChange?.deleted) {
@ -40,7 +42,7 @@ export const ThreadDeleteTrigger = automodTrigger<ThreadDeleteResult>()({
const parentName = matchResult.extra.matchedThreadParentName;
if (threadOwner) {
return `Thread **#${threadName ?? "Unknown"}** (\`${threadId}\`) created by **${escapeBold(
threadOwner.tag,
renderUsername(threadOwner),
)}** (\`${threadOwner.id}\`) in the **#${parentName}** (\`${parentId}\`) channel has been deleted`;
}
return `Thread **#${

View file

@ -1,6 +1,6 @@
import { User, escapeBold, type Snowflake } from "discord.js";
import * as t from "io-ts";
import { tNullable } from "../../../utils";
import z from "zod";
import { renderUsername } from "../../../utils";
import { automodTrigger } from "../helpers";
interface ThreadUnarchiveResult {
@ -11,12 +11,12 @@ interface ThreadUnarchiveResult {
matchedThreadOwner: User | undefined;
}
export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({
configType: t.type({
locked: tNullable(t.boolean),
}),
const configSchema = z.strictObject({
locked: z.boolean().optional(),
});
defaultConfig: {},
export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({
configSchema,
async match({ context, triggerConfig }) {
if (!context.threadChange?.unarchived) {
@ -48,7 +48,7 @@ export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({
const parentName = matchResult.extra.matchedThreadParentName;
const base = `Thread **#${threadName}** (\`${threadId}\`) has been unarchived in the **#${parentName}** (\`${parentId}\`) channel`;
if (threadOwner) {
return `${base} by **${escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`;
return `${base} by **${escapeBold(renderUsername(threadOwner))}** (\`${threadOwner.id}\`)`;
}
return base;
},

View file

@ -1,12 +1,13 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface UnbanTriggerResultType {}
const configSchema = z.strictObject({});
export const UnbanTrigger = automodTrigger<UnbanTriggerResultType>()({
configType: t.type({}),
defaultConfig: {},
configSchema,
async match({ context }) {
if (context.modAction?.type !== "unban") {

View file

@ -1,12 +1,13 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface UnmuteTriggerResultType {}
const configSchema = z.strictObject({});
export const UnmuteTrigger = automodTrigger<UnmuteTriggerResultType>()({
configType: t.type({}),
defaultConfig: {},
configSchema,
async match({ context }) {
if (context.modAction?.type !== "unmute") {

View file

@ -1,19 +1,16 @@
import * as t from "io-ts";
import z from "zod";
import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface
interface WarnTriggerResultType {}
export const WarnTrigger = automodTrigger<WarnTriggerResultType>()({
configType: t.type({
manual: t.boolean,
automatic: t.boolean,
}),
const configSchema = z.strictObject({
manual: z.boolean().default(true),
automatic: z.boolean().default(true),
});
defaultConfig: {
manual: true,
automatic: true,
},
export const WarnTrigger = automodTrigger<WarnTriggerResultType>()({
configSchema,
async match({ context, triggerConfig }) {
if (context.modAction?.type !== "warn") {

View file

@ -1,6 +1,6 @@
import { GuildMember, GuildTextBasedChannel, PartialGuildMember, ThreadChannel, User } from "discord.js";
import * as t from "io-ts";
import { BasePluginType, CooldownManager } from "knub";
import z from "zod";
import { Queue } from "../../Queue";
import { RegExpRunner } from "../../RegExpRunner";
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
@ -8,39 +8,80 @@ import { GuildArchives } from "../../data/GuildArchives";
import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { SavedMessage } from "../../data/entities/SavedMessage";
import { tNullable } from "../../utils";
import { entries, zBoundedRecord, zDelayString } from "../../utils";
import { CounterEvents } from "../Counters/types";
import { ModActionType, ModActionsEvents } from "../ModActions/types";
import { MutesEvents } from "../Mutes/types";
import { AvailableActions } from "./actions/availableActions";
import { availableActions } from "./actions/availableActions";
import { RecentActionType } from "./constants";
import { AvailableTriggers } from "./triggers/availableTriggers";
import { availableTriggers } from "./triggers/availableTriggers";
import Timeout = NodeJS.Timeout;
export const Rule = t.type({
enabled: t.boolean,
name: t.string,
presets: tNullable(t.array(t.string)),
affects_bots: t.boolean,
affects_self: t.boolean,
triggers: t.array(t.partial(AvailableTriggers.props)),
actions: t.partial(AvailableActions.props),
cooldown: tNullable(t.string),
allow_further_rules: t.boolean,
});
export type TRule = t.TypeOf<typeof Rule>;
export type ZTriggersMapHelper = {
[TriggerName in keyof typeof availableTriggers]: (typeof availableTriggers)[TriggerName]["configSchema"];
};
const zTriggersMap = z
.strictObject(
entries(availableTriggers).reduce((map, [triggerName, trigger]) => {
map[triggerName] = trigger.configSchema;
return map;
}, {} as ZTriggersMapHelper),
)
.partial();
export const ConfigSchema = t.type({
rules: t.record(t.string, Rule),
antiraid_levels: t.array(t.string),
can_set_antiraid: t.boolean,
can_view_antiraid: t.boolean,
type ZActionsMapHelper = {
[ActionName in keyof typeof availableActions]: (typeof availableActions)[ActionName]["configSchema"];
};
const zActionsMap = z
.strictObject(
entries(availableActions).reduce((map, [actionName, action]) => {
// @ts-expect-error TS can't infer this properly but it works fine thanks to our helper
map[actionName] = action.configSchema;
return map;
}, {} as ZActionsMapHelper),
)
.partial();
const zRule = z.strictObject({
enabled: z.boolean().default(true),
// Typed as "never" because you are not expected to supply this directly.
// The transform instead picks it up from the property key and the output type is a string.
name: z
.never()
.optional()
.transform((_, ctx) => {
const ruleName = String(ctx.path[ctx.path.length - 2]).trim();
if (!ruleName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Automod rules must have names",
});
return z.NEVER;
}
return ruleName;
}),
presets: z.array(z.string().max(100)).max(25).default([]),
affects_bots: z.boolean().default(false),
affects_self: z.boolean().default(false),
cooldown: zDelayString.nullable().default(null),
allow_further_rules: z.boolean().default(false),
triggers: z.array(zTriggersMap),
actions: zActionsMap.refine((v) => !(v.clean && v.start_thread), {
message: "Cannot have both clean and start_thread active at the same time",
}),
});
export type TRule = z.infer<typeof zRule>;
export const zAutomodConfig = z.strictObject({
rules: zBoundedRecord(z.record(z.string().max(100), zRule), 0, 255),
antiraid_levels: z.array(z.string().max(100)).max(10),
can_set_antiraid: z.boolean(),
can_view_antiraid: z.boolean(),
});
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export interface AutomodPluginType extends BasePluginType {
config: TConfigSchema;
config: z.output<typeof zAutomodConfig>;
customOverrideCriteria: {
antiraid_level?: string;