diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index bf83b13e..8a19b273 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -85,6 +85,7 @@ const defaultOptions = { can_deletecase: false, can_act_as_other: false, create_cases_for_manual_actions: true, + reason_aliases: {}, }, overrides: [ { @@ -113,6 +114,19 @@ const defaultOptions = { ], }; +/** + * Config preprocessor to fix values + */ +const configPreprocessor = (options) => { + if (options.config?.reason_aliases) { + options.config.reason_aliases = Object.fromEntries( + Object.entries(options.config.reason_aliases).map(([k, v]) => [k.toLowerCase(), v]), + ); + } + + return options; +}; + export const ModActionsPlugin = zeppelinGuildPlugin()({ name: "mod_actions", showInDocs: true, @@ -127,6 +141,7 @@ export const ModActionsPlugin = zeppelinGuildPlugin()({ dependencies: () => [TimeAndDatePlugin, CasesPlugin, MutesPlugin, LogsPlugin], configParser: makeIoTsConfigParser(ConfigSchema), defaultOptions, + configPreprocessor, events: [CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, PostAlertOnMemberJoinEvt, AuditLogEvents], diff --git a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts index 43575463..2cef2246 100644 --- a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts @@ -1,11 +1,12 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CaseTypes } from "../../../data/CaseTypes"; import { Case } from "../../../data/entities/Case"; -import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { renderUserUsername, resolveMember, resolveUser } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; +import { parseReason } from "../functions/parseReason"; import { modActionsCmd } from "../types"; const opts = { @@ -58,8 +59,8 @@ export const AddCaseCmd = modActionsCmd({ sendErrorMessage(pluginData, msg.channel, "Cannot add case: invalid case type"); return; } - - const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); + const config = pluginData.config.get(); + const reason = formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]); // Create the case const casesPlugin = pluginData.getPlugin(CasesPlugin); diff --git a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts index 4ddef154..4ca098eb 100644 --- a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts +++ b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts @@ -9,6 +9,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; import { isBanned } from "../functions/isBanned"; +import { parseReason } from "../functions/parseReason"; import { IgnoredEventType, modActionsCmd } from "../types"; const opts = { @@ -60,8 +61,8 @@ export const ForcebanCmd = modActionsCmd({ mod = args.mod; } - - const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); + const config = pluginData.config.get(); + const reason = formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]); ignoreEvent(pluginData, IgnoredEventType.Ban, user.id); pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, user.id); diff --git a/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts b/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts index c5ce37ac..08bb3e19 100644 --- a/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts @@ -9,6 +9,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; import { isBanned } from "../functions/isBanned"; +import { parseReason } from "../functions/parseReason"; import { IgnoredEventType, modActionsCmd } from "../types"; export const MassunbanCmd = modActionsCmd({ @@ -36,8 +37,10 @@ export const MassunbanCmd = modActionsCmd({ sendErrorMessage(pluginData, msg.channel, "Cancelled"); return; } - - const unbanReason = formatReasonWithAttachments(unbanReasonReply.content, [...msg.attachments.values()]); + const config = pluginData.config.get(); + const unbanReason = formatReasonWithAttachments(parseReason(config, unbanReasonReply.content), [ + ...msg.attachments.values(), + ]); // Ignore automatic unban cases and logs for these users // We'll create our own cases below and post a single "mass unbanned" log instead diff --git a/backend/src/plugins/ModActions/commands/MassmuteCmd.ts b/backend/src/plugins/ModActions/commands/MassmuteCmd.ts index a62c029e..8f3c181c 100644 --- a/backend/src/plugins/ModActions/commands/MassmuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassmuteCmd.ts @@ -7,6 +7,7 @@ import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginU import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; +import { parseReason } from "../functions/parseReason"; import { modActionsCmd } from "../types"; export const MassmuteCmd = modActionsCmd({ @@ -39,7 +40,10 @@ export const MassmuteCmd = modActionsCmd({ return; } - const muteReason = formatReasonWithAttachments(muteReasonReceived.content, [...msg.attachments.values()]); + const config = pluginData.config.get(); + const muteReason = formatReasonWithAttachments(parseReason(config, muteReasonReceived.content), [ + ...msg.attachments.values(), + ]); // Verify we can act upon all users for (const userId of args.userIds) { diff --git a/backend/src/plugins/ModActions/commands/UnbanCmd.ts b/backend/src/plugins/ModActions/commands/UnbanCmd.ts index 53363ee0..aaaf05cb 100644 --- a/backend/src/plugins/ModActions/commands/UnbanCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnbanCmd.ts @@ -9,6 +9,7 @@ import { resolveUser } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; +import { parseReason } from "../functions/parseReason"; import { IgnoredEventType, modActionsCmd } from "../types"; const opts = { @@ -48,7 +49,8 @@ export const UnbanCmd = modActionsCmd({ } pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, user.id); - const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); + const config = pluginData.config.get(); + const reason = formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]); try { ignoreEvent(pluginData, IgnoredEventType.Unban, user.id); diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index c8192015..e57023c3 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -6,6 +6,7 @@ import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { isBanned } from "../functions/isBanned"; +import { parseReason } from "../functions/parseReason"; import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs"; import { warnMember } from "../functions/warnMember"; import { modActionsCmd } from "../types"; @@ -62,7 +63,7 @@ export const WarnCmd = modActionsCmd({ } const config = pluginData.config.get(); - const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); + const reason = formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]); const casesPlugin = pluginData.getPlugin(CasesPlugin); const priorWarnAmount = await casesPlugin.getCaseTypeAmountForUserId(memberToWarn.id, CaseTypes.Warn); diff --git a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts index 73a1e2d9..98ff17a7 100644 --- a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts @@ -1,4 +1,4 @@ -import { GuildMember, GuildTextBasedChannel } from "discord.js"; +import { GuildMember, GuildTextBasedChannel, Message, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; import { hasPermission } from "knub/helpers"; import { LogType } from "../../../data/LogType"; @@ -9,11 +9,12 @@ import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; import { ignoreEvent } from "./ignoreEvent"; import { isBanned } from "./isBanned"; import { kickMember } from "./kickMember"; +import { parseReason } from "./parseReason"; import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs"; export async function actualKickMemberCmd( pluginData: GuildPluginData, - msg, + msg: Message, args: { user: string; reason: string; @@ -24,8 +25,9 @@ export async function actualKickMemberCmd( }, ) { const user = await resolveUser(pluginData.client, args.user); - if (!user.id) { - sendErrorMessage(pluginData, msg.channel, `User not found`); + const channel = msg.channel as TextChannel; + if (!user.id || !msg.member) { + sendErrorMessage(pluginData, channel, `User not found`); return; } @@ -34,9 +36,9 @@ export async function actualKickMemberCmd( if (!memberToKick) { const banned = await isBanned(pluginData, user.id); if (banned) { - sendErrorMessage(pluginData, msg.channel, `User is banned`); + sendErrorMessage(pluginData, channel, `User is banned`); } else { - sendErrorMessage(pluginData, msg.channel, `User not found on the server`); + sendErrorMessage(pluginData, channel, `User not found on the server`); } return; @@ -44,7 +46,7 @@ export async function actualKickMemberCmd( // Make sure we're allowed to kick this member if (!canActOn(pluginData, msg.member, memberToKick)) { - sendErrorMessage(pluginData, msg.channel, "Cannot kick: insufficient permissions"); + sendErrorMessage(pluginData, channel, "Cannot kick: insufficient permissions"); return; } @@ -52,7 +54,7 @@ export async function actualKickMemberCmd( let mod = msg.member; if (args.mod) { if (!(await hasPermission(await pluginData.config.getForMessage(msg), "can_act_as_other"))) { - sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod"); + sendErrorMessage(pluginData, channel, "You don't have permission to use -mod"); return; } @@ -63,17 +65,18 @@ export async function actualKickMemberCmd( try { contactMethods = readContactMethodsFromArgs(args); } catch (e) { - sendErrorMessage(pluginData, msg.channel, e.message); + sendErrorMessage(pluginData, channel, e.message); return; } - const reason = formatReasonWithAttachments(args.reason, msg.attachments); + const config = pluginData.config.get(); + const reason = formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]); const kickResult = await kickMember(pluginData, memberToKick, reason, { contactMethods, caseArgs: { modId: mod.id, - ppId: mod.id !== msg.author.id ? msg.author.id : null, + ppId: mod.id !== msg.author.id ? msg.author.id : undefined, }, }); @@ -84,7 +87,7 @@ export async function actualKickMemberCmd( try { await memberToKick.ban({ deleteMessageSeconds: (1 * DAYS) / SECONDS, reason: "kick -clean" }); } catch { - sendErrorMessage(pluginData, msg.channel, "Failed to ban the user to clean messages (-clean)"); + sendErrorMessage(pluginData, channel, "Failed to ban the user to clean messages (-clean)"); } pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, memberToKick.id); @@ -93,7 +96,7 @@ export async function actualKickMemberCmd( try { await pluginData.guild.bans.remove(memberToKick.id, "kick -clean"); } catch { - sendErrorMessage(pluginData, msg.channel, "Failed to unban the user after banning them (-clean)"); + sendErrorMessage(pluginData, channel, "Failed to unban the user after banning them (-clean)"); } } @@ -106,5 +109,5 @@ export async function actualKickMemberCmd( let response = `Kicked **${renderUserUsername(memberToKick.user)}** (Case #${kickResult.case.case_number})`; if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`; - sendSuccessMessage(pluginData, msg.channel, response); + sendSuccessMessage(pluginData, channel, response); } diff --git a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts index 5c628c4e..f34942fe 100644 --- a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts @@ -9,6 +9,7 @@ import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { MuteResult } from "../../Mutes/types"; import { ModActionsPluginType } from "../types"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; +import { parseReason } from "./parseReason"; import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs"; /** @@ -42,7 +43,10 @@ export async function actualMuteUserCmd( } const timeUntilUnmute = args.time && humanizeDuration(args.time); - const reason = args.reason ? formatReasonWithAttachments(args.reason, [...msg.attachments.values()]) : undefined; + const config = pluginData.config.get(); + const reason = args.reason + ? formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]) + : undefined; let muteResult: MuteResult; const mutesPlugin = pluginData.getPlugin(MutesPlugin); diff --git a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts index d70a219c..b76831ed 100644 --- a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts @@ -6,6 +6,7 @@ import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin"; import { UnknownUser, asSingleLine, renderUserUsername } from "../../../utils"; import { ModActionsPluginType } from "../types"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; +import { parseReason } from "./parseReason"; export async function actualUnmuteCmd( pluginData: GuildPluginData, @@ -27,7 +28,10 @@ export async function actualUnmuteCmd( pp = msg.author; } - const reason = args.reason ? formatReasonWithAttachments(args.reason, [...msg.attachments.values()]) : undefined; + const config = pluginData.config.get(); + const reason = args.reason + ? formatReasonWithAttachments(parseReason(config, args.reason), [...msg.attachments.values()]) + : undefined; const mutesPlugin = pluginData.getPlugin(MutesPlugin); const result = await mutesPlugin.unmuteUser(user.id, args.time, { diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index d9d1454b..0ab5b42e 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -22,6 +22,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { BanOptions, BanResult, IgnoredEventType, ModActionsPluginType } from "../types"; import { getDefaultContactMethods } from "./getDefaultContactMethods"; import { ignoreEvent } from "./ignoreEvent"; +import { parseReason } from "./parseReason"; /** * Ban the specified user id, whether or not they're actually on the server at the time. Generates a case. @@ -41,6 +42,7 @@ export async function banUserId( error: "Invalid user", }; } + reason &&= parseReason(config, reason); // Attempt to message the user *before* banning them, as doing it after may not be possible const member = await resolveMember(pluginData.client, pluginData.guild, userId); diff --git a/backend/src/plugins/ModActions/functions/kickMember.ts b/backend/src/plugins/ModActions/functions/kickMember.ts index d54dfd15..ffa4bb19 100644 --- a/backend/src/plugins/ModActions/functions/kickMember.ts +++ b/backend/src/plugins/ModActions/functions/kickMember.ts @@ -10,6 +10,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { IgnoredEventType, KickOptions, KickResult, ModActionsPluginType } from "../types"; import { getDefaultContactMethods } from "./getDefaultContactMethods"; import { ignoreEvent } from "./ignoreEvent"; +import { parseReason } from "./parseReason"; /** * Kick the specified server member. Generates a case. @@ -25,6 +26,7 @@ export async function kickMember( // Attempt to message the user *before* kicking them, as doing it after may not be possible let notifyResult: UserNotificationResult = { method: null, success: true }; if (reason && member) { + reason = parseReason(config, reason); const contactMethods = kickOptions?.contactMethods ? kickOptions.contactMethods : getDefaultContactMethods(pluginData, "kick"); diff --git a/backend/src/plugins/ModActions/functions/parseReason.ts b/backend/src/plugins/ModActions/functions/parseReason.ts new file mode 100644 index 00000000..a73b0ff3 --- /dev/null +++ b/backend/src/plugins/ModActions/functions/parseReason.ts @@ -0,0 +1,14 @@ +import { TConfigSchema } from "../types"; + +const MAX_REASON_LENGTH = 512; + +export function parseReason(config: TConfigSchema, reason: string): string { + if (!reason) return reason; + if (config?.reason_aliases) { + reason = config.reason_aliases[reason.toLowerCase()] ?? reason; + } + if (reason!.length > MAX_REASON_LENGTH) { + reason = reason!.substring(0, MAX_REASON_LENGTH - 4) + " […]"; + } + return reason; +} diff --git a/backend/src/plugins/ModActions/functions/updateCase.ts b/backend/src/plugins/ModActions/functions/updateCase.ts index c19c9d06..d6162117 100644 --- a/backend/src/plugins/ModActions/functions/updateCase.ts +++ b/backend/src/plugins/ModActions/functions/updateCase.ts @@ -1,13 +1,16 @@ import { Message } from "discord.js"; +import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../data/CaseTypes"; import { Case } from "../../../data/entities/Case"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { ModActionsPluginType } from "../types.js"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; +import { parseReason } from "./parseReason.js"; -export async function updateCase(pluginData, msg: Message, args) { - let theCase: Case | undefined; +export async function updateCase(pluginData: GuildPluginData, msg: Message, args) { + let theCase: Case | null; if (args.caseNumber != null) { theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber); } else { @@ -23,6 +26,8 @@ export async function updateCase(pluginData, msg: Message, args) { sendErrorMessage(pluginData, msg.channel, "Text or attachment required"); return; } + const config = pluginData.config.get(); + args.note &&= parseReason(config, args.note); const note = formatReasonWithAttachments(args.note, [...msg.attachments.values()]); diff --git a/backend/src/plugins/ModActions/functions/warnMember.ts b/backend/src/plugins/ModActions/functions/warnMember.ts index 9bc0fda9..98b172ed 100644 --- a/backend/src/plugins/ModActions/functions/warnMember.ts +++ b/backend/src/plugins/ModActions/functions/warnMember.ts @@ -9,6 +9,7 @@ import { CasesPlugin } from "../../Cases/CasesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { ModActionsPluginType, WarnOptions, WarnResult } from "../types"; import { getDefaultContactMethods } from "./getDefaultContactMethods"; +import { parseReason } from "./parseReason"; export async function warnMember( pluginData: GuildPluginData, @@ -17,7 +18,7 @@ export async function warnMember( warnOptions: WarnOptions = {}, ): Promise { const config = pluginData.config.get(); - + reason = parseReason(config, reason); let notifyResult: UserNotificationResult; if (config.warn_message) { const warnMessage = await renderTemplate( diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index 447b9638..67412f4d 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -44,6 +44,7 @@ export const ConfigSchema = t.type({ can_deletecase: t.boolean, can_act_as_other: t.boolean, create_cases_for_manual_actions: t.boolean, + reason_aliases: tNullable(t.record(t.string, t.string)), }); export type TConfigSchema = t.TypeOf;