Skip to content

Commit

Permalink
feat: handle template errors
Browse files Browse the repository at this point in the history
Fixes ZDEV-20
  • Loading branch information
Dragory committed Jan 27, 2024
1 parent 2ce5082 commit ffa9eeb
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 94 deletions.
2 changes: 2 additions & 0 deletions backend/src/RecoverablePluginError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum ERRORS {
MUTE_ROLE_ABOVE_ZEP,
USER_ABOVE_ZEP,
USER_NOT_MODERATABLE,
TEMPLATE_PARSE_ERROR,
}

export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
Expand All @@ -24,6 +25,7 @@ export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
[ERRORS.MUTE_ROLE_ABOVE_ZEP]: "Specified mute role is above Zeppelin in the role hierarchy",
[ERRORS.USER_ABOVE_ZEP]: "Cannot mute user, specified user is above Zeppelin in the role hierarchy",
[ERRORS.USER_NOT_MODERATABLE]: "Cannot mute user, specified user is not moderatable",
[ERRORS.TEMPLATE_PARSE_ERROR]: "Template parse error",
};

export class RecoverablePluginError extends Error {
Expand Down
55 changes: 39 additions & 16 deletions backend/src/plugins/Automod/actions/changePerms.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { PermissionsBitField, PermissionsString } from "discord.js";
import { U } from "ts-toolbelt";
import z from "zod";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
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"]>;
Expand Down Expand Up @@ -71,30 +72,52 @@ export const ChangePermsAction = automodAction({
perms: z.record(z.enum(allPermissionNames), z.boolean().nullable()),
}),

async apply({ pluginData, contexts, actionConfig }) {
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);
Expand Down
21 changes: 16 additions & 5 deletions backend/src/plugins/Automod/actions/reply.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js";
import z from "zod";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import {
convertDelayStringToMS,
noop,
Expand Down Expand Up @@ -58,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;
Expand Down
23 changes: 17 additions & 6 deletions backend/src/plugins/Automod/actions/startThread.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js";
import z from "zod";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
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[] = [
Expand All @@ -21,7 +22,7 @@ export const StartThreadAction = automodAction({
limit_per_channel: z.number().nullable().default(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;
Expand All @@ -48,15 +49,25 @@ export const StartThreadAction = automodAction({
const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id);
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,
Expand Down
2 changes: 2 additions & 0 deletions backend/src/plugins/CustomEvents/CustomEventsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
messageToTemplateSafeMessage,
userToTemplateSafeUser,
} from "../../utils/templateSafeObjects";
import { LogsPlugin } from "../Logs/LogsPlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
import { runEvent } from "./functions/runEvent";
import { CustomEventsPluginType, zCustomEventsConfig } from "./types";
Expand All @@ -25,6 +26,7 @@ export const CustomEventsPlugin = zeppelinGuildPlugin<CustomEventsPluginType>()(
name: "custom_events",
showInDocs: false,

dependencies: () => [LogsPlugin],
configParser: (input) => zCustomEventsConfig.parse(input),
defaultOptions,

Expand Down
6 changes: 5 additions & 1 deletion backend/src/plugins/CustomEvents/actions/addRoleAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { canActOn } from "../../../pluginUtils";
import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter";
import { resolveMember, zSnowflake } from "../../../utils";
import { ActionError } from "../ActionError";
import { catchTemplateError } from "../catchTemplateError";
import { CustomEventsPluginType, TCustomEvent } from "../types";

export const zAddRoleAction = z.strictObject({
Expand All @@ -20,7 +21,10 @@ export async function addRoleAction(
event: TCustomEvent,
eventData: any,
) {
const targetId = await renderTemplate(action.target, values, false);
const targetId = await catchTemplateError(
() => renderTemplate(action.target, values, false),
"Invalid target format",
);
const target = await resolveMember(pluginData.client, pluginData.guild, targetId);
if (!target) throw new ActionError(`Unknown target member: ${targetId}`);

Expand Down
11 changes: 7 additions & 4 deletions backend/src/plugins/CustomEvents/actions/createCaseAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFor
import { zBoundedCharacters, zSnowflake } from "../../../utils";
import { CasesPlugin } from "../../Cases/CasesPlugin";
import { ActionError } from "../ActionError";
import { catchTemplateError } from "../catchTemplateError";
import { CustomEventsPluginType, TCustomEvent } from "../types";

export const zCreateCaseAction = z.strictObject({
Expand All @@ -23,10 +24,12 @@ export async function createCaseAction(
event: TCustomEvent,
eventData: any, // eslint-disable-line @typescript-eslint/no-unused-vars
) {
const modId = await renderTemplate(action.mod, values, false);
const targetId = await renderTemplate(action.target, values, false);

const reason = await renderTemplate(action.reason, values, false);
const modId = await catchTemplateError(() => renderTemplate(action.mod, values, false), "Invalid mod format");
const targetId = await catchTemplateError(
() => renderTemplate(action.target, values, false),
"Invalid target format",
);
const reason = await catchTemplateError(() => renderTemplate(action.reason, values, false), "Invalid reason format");

if (CaseTypes[action.case_type] == null) {
throw new ActionError(`Invalid case type: ${action.type}`);
Expand Down
6 changes: 5 additions & 1 deletion backend/src/plugins/CustomEvents/actions/messageAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import z from "zod";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { zBoundedCharacters, zSnowflake } from "../../../utils";
import { ActionError } from "../ActionError";
import { catchTemplateError } from "../catchTemplateError";
import { CustomEventsPluginType } from "../types";

export const zMessageAction = z.strictObject({
Expand All @@ -18,7 +19,10 @@ export async function messageAction(
action: TMessageAction,
values: TemplateSafeValueContainer,
) {
const targetChannelId = await renderTemplate(action.channel, values, false);
const targetChannelId = await catchTemplateError(
() => renderTemplate(action.channel, values, false),
"Invalid channel format",
);
const targetChannel = pluginData.guild.channels.cache.get(targetChannelId as Snowflake);
if (!targetChannel) throw new ActionError("Unknown target channel");
if (!(targetChannel instanceof TextChannel)) throw new ActionError("Target channel is not a text channel");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { canActOn } from "../../../pluginUtils";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { resolveMember, zSnowflake } from "../../../utils";
import { ActionError } from "../ActionError";
import { catchTemplateError } from "../catchTemplateError";
import { CustomEventsPluginType, TCustomEvent } from "../types";

export const zMoveToVoiceChannelAction = z.strictObject({
Expand All @@ -21,15 +22,21 @@ export async function moveToVoiceChannelAction(
event: TCustomEvent,
eventData: any,
) {
const targetId = await renderTemplate(action.target, values, false);
const targetId = await catchTemplateError(
() => renderTemplate(action.target, values, false),
"Invalid target format",
);
const target = await resolveMember(pluginData.client, pluginData.guild, targetId);
if (!target) throw new ActionError("Unknown target member");

if (event.trigger.type === "command" && !canActOn(pluginData, eventData.msg.member, target)) {
throw new ActionError("Missing permissions");
}

const targetChannelId = await renderTemplate(action.channel, values, false);
const targetChannelId = await catchTemplateError(
() => renderTemplate(action.channel, values, false),
"Invalid channel format",
);
const targetChannel = pluginData.guild.channels.cache.get(targetChannelId as Snowflake);
if (!targetChannel) throw new ActionError("Unknown target channel");
if (!(targetChannel instanceof VoiceChannel)) throw new ActionError("Target channel is not a voice channel");
Expand Down
13 changes: 13 additions & 0 deletions backend/src/plugins/CustomEvents/catchTemplateError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TemplateParseError } from "../../templateFormatter";
import { ActionError } from "./ActionError";

export function catchTemplateError(fn: () => Promise<string>, errorText: string): Promise<string> {
try {
return fn();
} catch (err) {
if (err instanceof TemplateParseError) {
throw new ActionError(`${errorText}: ${err.message}`);
}
throw err;
}
}
66 changes: 44 additions & 22 deletions backend/src/plugins/ModActions/functions/banUserId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CaseTypes } from "../../../data/CaseTypes";
import { LogType } from "../../../data/LogType";
import { registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop";
import { logger } from "../../../logger";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import {
DAYS,
SECONDS,
Expand Down Expand Up @@ -52,30 +52,52 @@ export async function banUserId(

if (contactMethods.length) {
if (!banTime && config.ban_message) {
const banMessage = await renderTemplate(
config.ban_message,
new TemplateSafeValueContainer({
guildName: pluginData.guild.name,
reason,
moderator: banOptions.caseArgs?.modId
? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId))
: null,
}),
);
let banMessage: string;
try {
banMessage = await renderTemplate(
config.ban_message,
new TemplateSafeValueContainer({
guildName: pluginData.guild.name,
reason,
moderator: banOptions.caseArgs?.modId
? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId))
: null,
}),
);
} catch (err) {
if (err instanceof TemplateParseError) {
return {
status: "failed",
error: `Invalid ban_message format: ${err.message}`,
};
}
throw err;
}

notifyResult = await notifyUser(member.user, banMessage, contactMethods);
} else if (banTime && config.tempban_message) {
const banMessage = await renderTemplate(
config.tempban_message,
new TemplateSafeValueContainer({
guildName: pluginData.guild.name,
reason,
moderator: banOptions.caseArgs?.modId
? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId))
: null,
banTime: humanizeDuration(banTime),
}),
);
let banMessage: string;
try {
banMessage = await renderTemplate(
config.tempban_message,
new TemplateSafeValueContainer({
guildName: pluginData.guild.name,
reason,
moderator: banOptions.caseArgs?.modId
? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId))
: null,
banTime: humanizeDuration(banTime),
}),
);
} catch (err) {
if (err instanceof TemplateParseError) {
return {
status: "failed",
error: `Invalid tempban_message format: ${err.message}`,
};
}
throw err;
}

notifyResult = await notifyUser(member.user, banMessage, contactMethods);
} else {
Expand Down
Loading

0 comments on commit ffa9eeb

Please sign in to comment.