diff --git a/backend/src/RecoverablePluginError.ts b/backend/src/RecoverablePluginError.ts index 46a86ae5..64fed618 100644 --- a/backend/src/RecoverablePluginError.ts +++ b/backend/src/RecoverablePluginError.ts @@ -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 = { @@ -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 { diff --git a/backend/src/plugins/Automod/actions/changePerms.ts b/backend/src/plugins/Automod/actions/changePerms.ts index 7b7eba37..351b1637 100644 --- a/backend/src/plugins/Automod/actions/changePerms.ts +++ b/backend/src/plugins/Automod/actions/changePerms.ts @@ -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; @@ -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); diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts index e8b1fdf8..0628e26f 100644 --- a/backend/src/plugins/Automod/actions/reply.ts +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -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, @@ -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; diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 41648ad4..a521d72f 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -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[] = [ @@ -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; @@ -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 = { name: threadName, autoArchiveDuration: autoArchive, diff --git a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts index e17cd96b..82c5e612 100644 --- a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts +++ b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts @@ -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"; @@ -25,6 +26,7 @@ export const CustomEventsPlugin = zeppelinGuildPlugin()( name: "custom_events", showInDocs: false, + dependencies: () => [LogsPlugin], configParser: (input) => zCustomEventsConfig.parse(input), defaultOptions, diff --git a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts index f0870958..cd647687 100644 --- a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts +++ b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts @@ -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({ @@ -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}`); diff --git a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts index e894a446..a5e624fa 100644 --- a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts +++ b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts @@ -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({ @@ -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}`); diff --git a/backend/src/plugins/CustomEvents/actions/messageAction.ts b/backend/src/plugins/CustomEvents/actions/messageAction.ts index 40eee4b8..27780265 100644 --- a/backend/src/plugins/CustomEvents/actions/messageAction.ts +++ b/backend/src/plugins/CustomEvents/actions/messageAction.ts @@ -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({ @@ -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"); diff --git a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts index d42059f5..40adcb58 100644 --- a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts +++ b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts @@ -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({ @@ -21,7 +22,10 @@ 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"); @@ -29,7 +33,10 @@ export async function moveToVoiceChannelAction( 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"); diff --git a/backend/src/plugins/CustomEvents/catchTemplateError.ts b/backend/src/plugins/CustomEvents/catchTemplateError.ts new file mode 100644 index 00000000..015a4298 --- /dev/null +++ b/backend/src/plugins/CustomEvents/catchTemplateError.ts @@ -0,0 +1,13 @@ +import { TemplateParseError } from "../../templateFormatter"; +import { ActionError } from "./ActionError"; + +export function catchTemplateError(fn: () => Promise, errorText: string): Promise { + try { + return fn(); + } catch (err) { + if (err instanceof TemplateParseError) { + throw new ActionError(`${errorText}: ${err.message}`); + } + throw err; + } +} diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index d9d1454b..65686c81 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -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, @@ -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 { diff --git a/backend/src/plugins/ModActions/functions/kickMember.ts b/backend/src/plugins/ModActions/functions/kickMember.ts index d54dfd15..6dc26e35 100644 --- a/backend/src/plugins/ModActions/functions/kickMember.ts +++ b/backend/src/plugins/ModActions/functions/kickMember.ts @@ -2,7 +2,7 @@ import { GuildMember } from "discord.js"; import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../data/CaseTypes"; import { LogType } from "../../../data/LogType"; -import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; +import { renderTemplate, TemplateParseError, TemplateSafeValueContainer } from "../../../templateFormatter"; import { createUserNotificationError, notifyUser, resolveUser, ucfirst, UserNotificationResult } from "../../../utils"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { CasesPlugin } from "../../Cases/CasesPlugin"; @@ -31,16 +31,27 @@ export async function kickMember( if (contactMethods.length) { if (config.kick_message) { - const kickMessage = await renderTemplate( - config.kick_message, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason, - moderator: kickOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, kickOptions.caseArgs.modId)) - : null, - }), - ); + let kickMessage: string; + try { + kickMessage = await renderTemplate( + config.kick_message, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason, + moderator: kickOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, kickOptions.caseArgs.modId)) + : null, + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + return { + status: "failed", + error: `Invalid kick_message format: ${err.message}`, + }; + } + throw err; + } notifyResult = await notifyUser(member.user, kickMessage, contactMethods); } else { diff --git a/backend/src/plugins/ModActions/functions/warnMember.ts b/backend/src/plugins/ModActions/functions/warnMember.ts index 9bc0fda9..9aba15f4 100644 --- a/backend/src/plugins/ModActions/functions/warnMember.ts +++ b/backend/src/plugins/ModActions/functions/warnMember.ts @@ -1,7 +1,7 @@ import { GuildMember, Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../data/CaseTypes"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { UserNotificationResult, createUserNotificationError, notifyUser, resolveUser, ucfirst } from "../../../utils"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; @@ -20,16 +20,27 @@ export async function warnMember( let notifyResult: UserNotificationResult; if (config.warn_message) { - const warnMessage = await renderTemplate( - config.warn_message, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason, - moderator: warnOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, warnOptions.caseArgs.modId)) - : null, - }), - ); + let warnMessage: string; + try { + warnMessage = await renderTemplate( + config.warn_message, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason, + moderator: warnOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, warnOptions.caseArgs.modId)) + : null, + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + return { + status: "failed", + error: `Invalid warn_message format: ${err.message}`, + }; + } + throw err; + } const contactMethods = warnOptions?.contactMethods ? warnOptions.contactMethods : getDefaultContactMethods(pluginData, "warn"); diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts index 575f96fb..fcc27307 100644 --- a/backend/src/plugins/Mutes/functions/muteUser.ts +++ b/backend/src/plugins/Mutes/functions/muteUser.ts @@ -9,7 +9,7 @@ import { Case } from "../../../data/entities/Case"; import { Mute } from "../../../data/entities/Mute"; import { registerExpiringMute } from "../../../data/loops/expiringMutesLoop"; import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { UserNotificationMethod, UserNotificationResult, @@ -61,9 +61,10 @@ export async function muteUser( const member = await resolveMember(pluginData.client, pluginData.guild, user.id, true); // Grab the fresh member so we don't have stale role info const config = await pluginData.config.getMatchingConfig({ member, userId }); + const logs = pluginData.getPlugin(LogsPlugin); + let rolesToRestore: string[] = []; if (member) { - const logs = pluginData.getPlugin(LogsPlugin); // remove and store any roles to be removed/restored const currentUserRoles = [...member.roles.cache.keys()]; let newRoles: string[] = currentUserRoles; @@ -187,19 +188,31 @@ export async function muteUser( ? config.timed_mute_message : config.mute_message; - const muteMessage = - template && - (await renderTemplate( - template, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason: reason || "None", - time: timeUntilUnmuteStr, - moderator: muteOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, muteOptions.caseArgs.modId)) - : null, - }), - )); + let muteMessage: string | null = null; + try { + muteMessage = + template && + (await renderTemplate( + template, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason: reason || "None", + time: timeUntilUnmuteStr, + moderator: muteOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, muteOptions.caseArgs.modId)) + : null, + }), + )); + } catch (err) { + if (err instanceof TemplateParseError) { + logs.logBotAlert({ + body: `Invalid mute message format. The mute was still applied: ${err.message}`, + }); + } else { + lock.unlock(); + throw err; + } + } if (muteMessage && member) { let contactMethods: UserNotificationMethod[] = [];