diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 8702ad9b..ad496af8 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -1,4 +1,10 @@ -import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js"; +import { + ChannelType, + GuildTextThreadCreateOptions, + TextChannel, + ThreadAutoArchiveDuration, + ThreadChannel, +} from "discord.js"; import z from "zod"; import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils"; diff --git a/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts b/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts index 95b94c97..8c6cbdae 100644 --- a/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts +++ b/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts @@ -1,23 +1,30 @@ import { PluginOptions, guildPlugin } from "knub"; +import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks"; import { LogsPlugin } from "../Logs/LogsPlugin"; -import { ModActionsPlugin } from "../ModActions/ModActionsPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin"; import { UtilityPlugin } from "../Utility/UtilityPlugin"; +import { ContextClickedEvt } from "./events/ContextClickedEvt"; import { ContextMenuPluginType, zContextMenusConfig } from "./types"; +import { loadAllCommands } from "./utils/loadAllCommands"; const defaultOptions: PluginOptions = { config: { can_use: false, - can_open_mod_menu: false, + user_muteindef: false, + user_mute1d: false, + user_mute1h: false, + user_info: false, + + message_clean10: false, + message_clean25: false, + message_clean50: false, }, overrides: [ { level: ">=50", config: { can_use: true, - - can_open_mod_menu: true, }, }, ], @@ -26,15 +33,22 @@ const defaultOptions: PluginOptions = { export const ContextMenuPlugin = guildPlugin()({ name: "context_menu", - dependencies: () => [CasesPlugin, MutesPlugin, ModActionsPlugin, LogsPlugin, UtilityPlugin], + dependencies: () => [MutesPlugin, LogsPlugin, UtilityPlugin], configParser: (input) => zContextMenusConfig.parse(input), defaultOptions, - contextMenuCommands: [ModMenuCmd, NoteCmd, WarnCmd, MuteCmd, BanCmd], + // prettier-ignore + events: [ + ContextClickedEvt, + ], beforeLoad(pluginData) { const { state, guild } = pluginData; - state.cases = GuildCases.getGuildInstance(guild.id); + state.contextMenuLinks = new GuildContextMenuLinks(guild.id); + }, + + afterLoad(pluginData) { + loadAllCommands(pluginData); }, }); diff --git a/backend/src/plugins/ContextMenus/actions/ban.ts b/backend/src/plugins/ContextMenus/actions/ban.ts deleted file mode 100644 index ac423d00..00000000 --- a/backend/src/plugins/ContextMenus/actions/ban.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - ActionRowBuilder, - ButtonInteraction, - ContextMenuCommandInteraction, - ModalBuilder, - ModalSubmitInteraction, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; -import humanizeDuration from "humanize-duration"; -import { GuildPluginData } from "knub"; -import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin"; -import { logger } from "../../../logger"; -import { convertDelayStringToMS, renderUserUsername, resolveUser } from "../../../utils"; -import { CaseArgs } from "../../Cases/types"; -import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd"; -import { ContextMenuPluginType, ModMenuActionType } from "../types"; - -async function banAction( - pluginData: GuildPluginData, - duration: string | undefined, - reason: string | undefined, - target: string, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - submitInteraction: ModalSubmitInteraction, -) { - const interactionToReply = interaction.isButton() ? interaction : submitInteraction; - const executingMember = await pluginData.guild.members.fetch(interaction.user.id); - const userCfg = await pluginData.config.getMatchingConfig({ - channelId: interaction.channelId, - member: executingMember, - }); - - const modactions = pluginData.getPlugin(ModActionsPlugin); - if (!userCfg.can_use || !(await modactions.hasBanPermission(executingMember, interaction.channelId))) { - await interactionToReply - .editReply({ content: "Cannot ban: insufficient permissions", embeds: [], components: [] }) - .catch((err) => logger.error(`Ban interaction reply failed: ${err}`)); - return; - } - - const user = await resolveUser(pluginData.client, target); - if (!user.id) { - await interactionToReply - .editReply({ content: "User not found", embeds: [], components: [] }) - .catch((err) => logger.error(`Ban interaction reply failed: ${err}`)); - return; - } - - const caseArgs: Partial = { - modId: executingMember.id, - }; - - const durationMs = duration ? convertDelayStringToMS(duration)! : undefined; - const result = await modactions.banUserId(user.id, reason, { caseArgs }, durationMs); - if (result.status === "failed") { - await interactionToReply - .editReply({ content: "Error: Failed to ban user", embeds: [], components: [] }) - .catch((err) => logger.error(`Ban interaction reply failed: ${err}`)); - return; - } - - const userName = renderUserUsername(user); - const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : ""; - const banMessage = `Banned **${userName}** ${ - durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely" - } (Case #${result.case.case_number})${messageResultText}`; - - await interactionToReply - .editReply({ content: banMessage, embeds: [], components: [] }) - .catch((err) => logger.error(`Ban interaction reply failed: ${err}`)); -} - -export async function launchBanActionModal( - pluginData: GuildPluginData, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - target: string, -) { - const modalId = `${ModMenuActionType.BAN}:${interaction.id}`; - const modal = new ModalBuilder().setCustomId(modalId).setTitle("Ban"); - const durationIn = new TextInputBuilder() - .setCustomId("duration") - .setLabel("Duration (Optional)") - .setRequired(false) - .setStyle(TextInputStyle.Short); - const reasonIn = new TextInputBuilder() - .setCustomId("reason") - .setLabel("Reason (Optional)") - .setRequired(false) - .setStyle(TextInputStyle.Paragraph); - const durationRow = new ActionRowBuilder().addComponents(durationIn); - const reasonRow = new ActionRowBuilder().addComponents(reasonIn); - modal.addComponents(durationRow, reasonRow); - - await interaction.showModal(modal); - await interaction - .awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId }) - .then(async (submitted) => { - if (interaction.isButton()) { - await submitted.deferUpdate().catch((err) => logger.error(`Ban interaction defer failed: ${err}`)); - } else if (interaction.isContextMenuCommand()) { - await submitted - .deferReply({ ephemeral: true }) - .catch((err) => logger.error(`Ban interaction defer failed: ${err}`)); - } - - logger.info(`TargeT: ${target}`); - - const duration = submitted.fields.getTextInputValue("duration"); - const reason = submitted.fields.getTextInputValue("reason"); - - await banAction(pluginData, duration, reason, target, interaction, submitted); - }) - .catch((err) => logger.error(`Ban modal interaction failed: ${err}`)); -} diff --git a/backend/src/plugins/ContextMenus/actions/clean.ts b/backend/src/plugins/ContextMenus/actions/clean.ts index c5125721..a9d2384b 100644 --- a/backend/src/plugins/ContextMenus/actions/clean.ts +++ b/backend/src/plugins/ContextMenus/actions/clean.ts @@ -1,26 +1,16 @@ -import { - ActionRowBuilder, - Message, - MessageContextMenuCommandInteraction, - ModalBuilder, - ModalSubmitInteraction, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; +import { ContextMenuCommandInteraction, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; -import { logger } from "../../../logger"; +import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin"; -import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd"; -import { ContextMenuPluginType, ModMenuActionType } from "../types"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { ContextMenuPluginType } from "../types"; export async function cleanAction( pluginData: GuildPluginData, amount: number, - target: string, - targetMessage: Message, - targetChannel: string, - interaction: ModalSubmitInteraction, + interaction: ContextMenuCommandInteraction, ) { + await interaction.deferReply({ ephemeral: true }); const executingMember = await pluginData.guild.members.fetch(interaction.user.id); const userCfg = await pluginData.config.getMatchingConfig({ channelId: interaction.channelId, @@ -28,59 +18,33 @@ export async function cleanAction( }); const utility = pluginData.getPlugin(UtilityPlugin); - if (!userCfg.can_use || !(await utility.hasPermission(executingMember, targetChannel, "can_clean"))) { - await interaction - .editReply({ content: "Cannot clean: insufficient permissions", embeds: [], components: [] }) - .catch((err) => logger.error(`Clean interaction reply failed: ${err}`)); + if (!userCfg.can_use || !(await utility.hasPermission(executingMember, interaction.channelId, "can_clean"))) { + await interaction.followUp({ content: "Cannot clean: insufficient permissions" }); return; } - await interaction - .editReply({ - content: `Cleaning ${amount} messages from ${target}...`, - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Clean interaction reply failed: ${err}`)); - - await utility.clean({ count: amount, channel: targetChannel, "response-interaction": interaction }, targetMessage); -} - -export async function launchCleanActionModal( - pluginData: GuildPluginData, - interaction: MessageContextMenuCommandInteraction, - target: string, -) { - const modalId = `${ModMenuActionType.CLEAN}:${interaction.id}`; - const modal = new ModalBuilder().setCustomId(modalId).setTitle("Clean"); - const amountIn = new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short); - const amountRow = new ActionRowBuilder().addComponents(amountIn); - modal.addComponents(amountRow); - - await interaction.showModal(modal); - await interaction - .awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId }) - .then(async (submitted) => { - await submitted - .deferReply({ ephemeral: true }) - .catch((err) => logger.error(`Clean interaction defer failed: ${err}`)); - - const amount = submitted.fields.getTextInputValue("amount"); - if (isNaN(Number(amount))) { - interaction - .editReply({ content: `Error: Amount '${amount}' is invalid`, embeds: [], components: [] }) - .catch((err) => logger.error(`Clean interaction reply failed: ${err}`)); - return; - } - - await cleanAction( - pluginData, - Number(amount), - target, - interaction.targetMessage, - interaction.channelId, - submitted, + const targetMessage = interaction.channel + ? await interaction.channel.messages.fetch(interaction.targetId) + : await (pluginData.guild.channels.resolve(interaction.channelId) as TextChannel).messages.fetch( + interaction.targetId, ); - }) - .catch((err) => logger.error(`Clean modal interaction failed: ${err}`)); + + const targetUserOnly = false; + const deletePins = false; + const user = undefined; + + try { + await interaction.followUp(`Cleaning... Amount: ${amount}, User Only: ${targetUserOnly}, Pins: ${deletePins}`); + utility.clean({ count: amount, user, channel: targetMessage.channel.id, "delete-pins": deletePins }, targetMessage); + } catch (e) { + await interaction.followUp({ ephemeral: true, content: "Plugin error, please check your BOT_ALERTs" }); + + if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) { + pluginData.getPlugin(LogsPlugin).logBotAlert({ + body: `Failed to clean in <#${interaction.channelId}> in ContextMenu action \`clean\`:_ ${e}`, + }); + } else { + throw e; + } + } } diff --git a/backend/src/plugins/ContextMenus/actions/mute.ts b/backend/src/plugins/ContextMenus/actions/mute.ts index 1e444b4d..16b48a4a 100644 --- a/backend/src/plugins/ContextMenus/actions/mute.ts +++ b/backend/src/plugins/ContextMenus/actions/mute.ts @@ -1,33 +1,21 @@ -import { - ActionRowBuilder, - ButtonInteraction, - ContextMenuCommandInteraction, - ModalBuilder, - ModalSubmitInteraction, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; +import { ContextMenuCommandInteraction } from "discord.js"; import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { logger } from "../../../logger"; +import { canActOn } from "../../../pluginUtils"; import { convertDelayStringToMS } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; -import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd"; -import { ContextMenuPluginType, ModMenuActionType } from "../types"; +import { ContextMenuPluginType } from "../types"; -async function muteAction( +export async function muteAction( pluginData: GuildPluginData, duration: string | undefined, - reason: string | undefined, - target: string, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - submitInteraction: ModalSubmitInteraction, + interaction: ContextMenuCommandInteraction, ) { - const interactionToReply = interaction.isButton() ? interaction : submitInteraction; + await interaction.deferReply({ ephemeral: true }); const executingMember = await pluginData.guild.members.fetch(interaction.user.id); const userCfg = await pluginData.config.getMatchingConfig({ channelId: interaction.channelId, @@ -36,101 +24,43 @@ async function muteAction( const modactions = pluginData.getPlugin(ModActionsPlugin); if (!userCfg.can_use || !(await modactions.hasMutePermission(executingMember, interaction.channelId))) { - await interactionToReply - .editReply({ - content: "Cannot mute: insufficient permissions", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Mute interaction reply failed: ${err}`)); + await interaction.followUp({ content: "Cannot mute: insufficient permissions" }); return; } - const targetMember = await pluginData.guild.members.fetch(target); + const durationMs = duration ? convertDelayStringToMS(duration)! : undefined; + const mutes = pluginData.getPlugin(MutesPlugin); + const userId = interaction.targetId; + const targetMember = await pluginData.guild.members.fetch(interaction.targetId); + if (!canActOn(pluginData, executingMember, targetMember)) { - await interactionToReply - .editReply({ - content: "Cannot mute: insufficient permissions", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Mute interaction reply failed: ${err}`)); + await interaction.followUp({ ephemeral: true, content: "Cannot mute: insufficient permissions" }); return; } const caseArgs: Partial = { modId: executingMember.id, }; - const mutes = pluginData.getPlugin(MutesPlugin); - const durationMs = duration ? convertDelayStringToMS(duration)! : undefined; try { - const result = await mutes.muteUser(targetMember.id, durationMs, reason, { caseArgs }); + const result = await mutes.muteUser(userId, durationMs, "Context Menu Action", { caseArgs }); - const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : ""; - const muteMessage = `Muted **${result.case.user_name}** ${ + const muteMessage = `Muted **${result.case!.user_name}** ${ durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely" - } (Case #${result.case.case_number})${messageResultText}`; + } (Case #${result.case!.case_number}) (user notified via ${ + result.notifyResult.method ?? "dm" + })\nPlease update the new case with the \`update\` command`; - await interactionToReply - .editReply({ content: muteMessage, embeds: [], components: [] }) - .catch((err) => logger.error(`Mute interaction reply failed: ${err}`)); + await interaction.followUp({ ephemeral: true, content: muteMessage }); } catch (e) { - await interactionToReply - .editReply({ - content: "Plugin error, please check your BOT_ALERTs", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Mute interaction reply failed: ${err}`)); + await interaction.followUp({ ephemeral: true, content: "Plugin error, please check your BOT_ALERTs" }); if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) { pluginData.getPlugin(LogsPlugin).logBotAlert({ - body: `Failed to mute <@!${target}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`, + body: `Failed to mute <@!${userId}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`, }); } else { throw e; } } } - -export async function launchMuteActionModal( - pluginData: GuildPluginData, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - target: string, -) { - const modalId = `${ModMenuActionType.MUTE}:${interaction.id}`; - const modal = new ModalBuilder().setCustomId(modalId).setTitle("Mute"); - const durationIn = new TextInputBuilder() - .setCustomId("duration") - .setLabel("Duration (Optional)") - .setRequired(false) - .setStyle(TextInputStyle.Short); - const reasonIn = new TextInputBuilder() - .setCustomId("reason") - .setLabel("Reason (Optional)") - .setRequired(false) - .setStyle(TextInputStyle.Paragraph); - const durationRow = new ActionRowBuilder().addComponents(durationIn); - const reasonRow = new ActionRowBuilder().addComponents(reasonIn); - modal.addComponents(durationRow, reasonRow); - - await interaction.showModal(modal); - await interaction - .awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId }) - .then(async (submitted) => { - if (interaction.isButton()) { - await submitted.deferUpdate().catch((err) => logger.error(`Mute interaction defer failed: ${err}`)); - } else if (interaction.isContextMenuCommand()) { - await submitted - .deferReply({ ephemeral: true }) - .catch((err) => logger.error(`Mute interaction defer failed: ${err}`)); - } - - const duration = submitted.fields.getTextInputValue("duration"); - const reason = submitted.fields.getTextInputValue("reason"); - - await muteAction(pluginData, duration, reason, target, interaction, submitted); - }) - .catch((err) => logger.error(`Mute modal interaction failed: ${err}`)); -} diff --git a/backend/src/plugins/ContextMenus/actions/note.ts b/backend/src/plugins/ContextMenus/actions/note.ts deleted file mode 100644 index b6911274..00000000 --- a/backend/src/plugins/ContextMenus/actions/note.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - ActionRowBuilder, - ButtonInteraction, - ContextMenuCommandInteraction, - ModalBuilder, - ModalSubmitInteraction, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; -import { GuildPluginData } from "knub"; -import { canActOn } from "src/pluginUtils"; -import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin"; -import { CaseTypes } from "../../../data/CaseTypes"; -import { logger } from "../../../logger"; -import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; -import { renderUserUsername } from "../../../utils"; -import { LogsPlugin } from "../../Logs/LogsPlugin"; -import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd"; -import { ContextMenuPluginType, ModMenuActionType } from "../types"; - -async function noteAction( - pluginData: GuildPluginData, - reason: string, - target: string, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - submitInteraction: ModalSubmitInteraction, -) { - const interactionToReply = interaction.isButton() ? interaction : submitInteraction; - const executingMember = await pluginData.guild.members.fetch(interaction.user.id); - const userCfg = await pluginData.config.getMatchingConfig({ - channelId: interaction.channelId, - member: executingMember, - }); - - const modactions = pluginData.getPlugin(ModActionsPlugin); - if (!userCfg.can_use || !(await modactions.hasNotePermission(executingMember, interaction.channelId))) { - await interactionToReply - .editReply({ - content: "Cannot note: insufficient permissions", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Note interaction reply failed: ${err}`)); - return; - } - - const targetMember = await pluginData.guild.members.fetch(target); - if (!canActOn(pluginData, executingMember, targetMember)) { - await interactionToReply - .editReply({ - content: "Cannot note: insufficient permissions", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Note interaction reply failed: ${err}`)); - return; - } - - const casesPlugin = pluginData.getPlugin(CasesPlugin); - const createdCase = await casesPlugin.createCase({ - userId: target, - modId: executingMember.id, - type: CaseTypes.Note, - reason, - }); - - pluginData.getPlugin(LogsPlugin).logMemberNote({ - mod: interaction.user, - user: targetMember.user, - caseNumber: createdCase.case_number, - reason, - }); - - const userName = renderUserUsername(targetMember.user); - await interactionToReply - .editReply({ - content: `Note added on **${userName}** (Case #${createdCase.case_number})`, - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Note interaction reply failed: ${err}`)); -} - -export async function launchNoteActionModal( - pluginData: GuildPluginData, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - target: string, -) { - const modalId = `${ModMenuActionType.NOTE}:${interaction.id}`; - const modal = new ModalBuilder().setCustomId(modalId).setTitle("Note"); - const reasonIn = new TextInputBuilder().setCustomId("reason").setLabel("Note").setStyle(TextInputStyle.Paragraph); - const reasonRow = new ActionRowBuilder().addComponents(reasonIn); - modal.addComponents(reasonRow); - - await interaction.showModal(modal); - await interaction - .awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId }) - .then(async (submitted) => { - if (interaction.isButton()) { - await submitted.deferUpdate().catch((err) => logger.error(`Note interaction defer failed: ${err}`)); - } else if (interaction.isContextMenuCommand()) { - await submitted - .deferReply({ ephemeral: true }) - .catch((err) => logger.error(`Note interaction defer failed: ${err}`)); - } - - const reason = submitted.fields.getTextInputValue("reason"); - - await noteAction(pluginData, reason, target, interaction, submitted); - }) - .catch((err) => logger.error(`Note modal interaction failed: ${err}`)); -} diff --git a/backend/src/plugins/ContextMenus/actions/update.ts b/backend/src/plugins/ContextMenus/actions/update.ts deleted file mode 100644 index 3365f293..00000000 --- a/backend/src/plugins/ContextMenus/actions/update.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GuildMember } from "discord.js"; -import { GuildPluginData } from "knub"; -import { CaseTypes } from "../../../data/CaseTypes"; -import { Case } from "../../../data/entities/Case"; -import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; -import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin"; -import { ContextMenuPluginType } from "../types"; - -export async function updateAction( - pluginData: GuildPluginData, - executingMember: GuildMember, - theCase: Case, - value: string, -) { - const casesPlugin = pluginData.getPlugin(CasesPlugin); - await casesPlugin.createCaseNote({ - caseId: theCase.case_number, - modId: executingMember.id, - body: value, - }); - - pluginData.getPlugin(LogsPlugin).logCaseUpdate({ - mod: executingMember.user, - caseNumber: theCase.case_number, - caseType: CaseTypes[theCase.type], - note: value, - }); -} diff --git a/backend/src/plugins/ContextMenus/actions/warn.ts b/backend/src/plugins/ContextMenus/actions/warn.ts deleted file mode 100644 index d8eba234..00000000 --- a/backend/src/plugins/ContextMenus/actions/warn.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - ActionRowBuilder, - ButtonInteraction, - ContextMenuCommandInteraction, - ModalBuilder, - ModalSubmitInteraction, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; -import { GuildPluginData } from "knub"; -import { canActOn } from "src/pluginUtils"; -import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin"; -import { logger } from "../../../logger"; -import { renderUserUsername } from "../../../utils"; -import { CaseArgs } from "../../Cases/types"; -import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd"; -import { ContextMenuPluginType, ModMenuActionType } from "../types"; - -async function warnAction( - pluginData: GuildPluginData, - reason: string, - target: string, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - submitInteraction: ModalSubmitInteraction, -) { - const interactionToReply = interaction.isButton() ? interaction : submitInteraction; - const executingMember = await pluginData.guild.members.fetch(interaction.user.id); - const userCfg = await pluginData.config.getMatchingConfig({ - channelId: interaction.channelId, - member: executingMember, - }); - - const modactions = pluginData.getPlugin(ModActionsPlugin); - if (!userCfg.can_use || !(await modactions.hasWarnPermission(executingMember, interaction.channelId))) { - await interactionToReply - .editReply({ - content: "Cannot warn: insufficient permissions", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Warn interaction reply failed: ${err}`)); - return; - } - - const targetMember = await pluginData.guild.members.fetch(target); - if (!canActOn(pluginData, executingMember, targetMember)) { - await interactionToReply - .editReply({ - content: "Cannot warn: insufficient permissions", - embeds: [], - components: [], - }) - .catch((err) => logger.error(`Warn interaction reply failed: ${err}`)); - return; - } - - const caseArgs: Partial = { - modId: executingMember.id, - }; - - const result = await modactions.warnMember(targetMember, reason, { caseArgs }); - if (result.status === "failed") { - await interactionToReply - .editReply({ content: "Error: Failed to warn user", embeds: [], components: [] }) - .catch((err) => logger.error(`Warn interaction reply failed: ${err}`)); - return; - } - - const userName = renderUserUsername(targetMember.user); - const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : ""; - const muteMessage = `Warned **${userName}** (Case #${result.case.case_number})${messageResultText}`; - - await interactionToReply - .editReply({ content: muteMessage, embeds: [], components: [] }) - .catch((err) => logger.error(`Warn interaction reply failed: ${err}`)); -} - -export async function launchWarnActionModal( - pluginData: GuildPluginData, - interaction: ButtonInteraction | ContextMenuCommandInteraction, - target: string, -) { - const modalId = `${ModMenuActionType.WARN}:${interaction.id}`; - const modal = new ModalBuilder().setCustomId(modalId).setTitle("Warn"); - const reasonIn = new TextInputBuilder().setCustomId("reason").setLabel("Reason").setStyle(TextInputStyle.Paragraph); - const reasonRow = new ActionRowBuilder().addComponents(reasonIn); - modal.addComponents(reasonRow); - - await interaction.showModal(modal); - await interaction - .awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId }) - .then(async (submitted) => { - if (interaction.isButton()) { - await submitted.deferUpdate().catch((err) => logger.error(`Warn interaction defer failed: ${err}`)); - } else if (interaction.isContextMenuCommand()) { - await submitted - .deferReply({ ephemeral: true }) - .catch((err) => logger.error(`Warn interaction defer failed: ${err}`)); - } - - const reason = submitted.fields.getTextInputValue("reason"); - - await warnAction(pluginData, reason, target, interaction, submitted); - }) - .catch((err) => logger.error(`Warn modal interaction failed: ${err}`)); -} diff --git a/backend/src/plugins/ContextMenus/commands/BanUserCtxCmd.ts b/backend/src/plugins/ContextMenus/commands/BanUserCtxCmd.ts deleted file mode 100644 index 6da32f8d..00000000 --- a/backend/src/plugins/ContextMenus/commands/BanUserCtxCmd.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PermissionFlagsBits } from "discord.js"; -import { guildPluginUserContextMenuCommand } from "knub"; -import { launchBanActionModal } from "../actions/ban"; - -export const BanCmd = guildPluginUserContextMenuCommand({ - name: "Ban", - defaultMemberPermissions: PermissionFlagsBits.ModerateMembers.toString(), - async run({ pluginData, interaction }) { - await launchBanActionModal(pluginData, interaction, interaction.targetId); - }, -}); diff --git a/backend/src/plugins/ContextMenus/commands/CleanMessageCtxCmd.ts b/backend/src/plugins/ContextMenus/commands/CleanMessageCtxCmd.ts deleted file mode 100644 index 0902ab00..00000000 --- a/backend/src/plugins/ContextMenus/commands/CleanMessageCtxCmd.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PermissionFlagsBits } from "discord.js"; -import { guildPluginMessageContextMenuCommand } from "knub"; -import { launchCleanActionModal } from "../actions/clean"; - -export const CleanCmd = guildPluginMessageContextMenuCommand({ - name: "Clean", - defaultMemberPermissions: PermissionFlagsBits.ManageMessages.toString(), - async run({ pluginData, interaction }) { - await launchCleanActionModal(pluginData, interaction, interaction.targetId); - }, -}); diff --git a/backend/src/plugins/ContextMenus/commands/ModMenuUserCtxCmd.ts b/backend/src/plugins/ContextMenus/commands/ModMenuUserCtxCmd.ts deleted file mode 100644 index 9cb40f14..00000000 --- a/backend/src/plugins/ContextMenus/commands/ModMenuUserCtxCmd.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { - APIEmbed, - ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - ContextMenuCommandInteraction, - GuildMember, - PermissionFlagsBits, - User, -} from "discord.js"; -import { GuildPluginData, guildPluginUserContextMenuCommand } from "knub"; -import { Case } from "../../../data/entities/Case"; -import { logger } from "../../../logger"; -import { ModActionsPlugin } from "../../../plugins/ModActions/ModActionsPlugin"; -import { SECONDS, UnknownUser, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils"; -import { asyncMap } from "../../../utils/async"; -import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; -import { getGuildPrefix } from "../../../utils/getGuildPrefix"; -import { CasesPlugin } from "../../Cases/CasesPlugin"; -import { UtilityPlugin } from "../../Utility/UtilityPlugin"; -import { getUserInfoEmbed } from "../../Utility/functions/getUserInfoEmbed"; -import { launchBanActionModal } from "../actions/ban"; -import { launchMuteActionModal } from "../actions/mute"; -import { launchNoteActionModal } from "../actions/note"; -import { launchWarnActionModal } from "../actions/warn"; -import { - ContextMenuPluginType, - LoadModMenuPageFn, - ModMenuActionOpts, - ModMenuActionType, - ModMenuNavigationType, -} from "../types"; - -export const MODAL_TIMEOUT = 60 * SECONDS; -const MOD_MENU_TIMEOUT = 60 * SECONDS; -const CASES_PER_PAGE = 10; - -export const ModMenuCmd = guildPluginUserContextMenuCommand({ - name: "Mod Menu", - defaultMemberPermissions: PermissionFlagsBits.ViewAuditLog.toString(), - async run({ pluginData, interaction }) { - await interaction - .deferReply({ ephemeral: true }) - .catch((err) => logger.error(`Mod menu interaction defer failed: ${err}`)); - - // Run permission checks for executing user. - const executingMember = await pluginData.guild.members.fetch(interaction.user.id); - const userCfg = await pluginData.config.getMatchingConfig({ - channelId: interaction.channelId, - member: executingMember, - }); - const utility = pluginData.getPlugin(UtilityPlugin); - if ( - !userCfg.can_use || - (await !utility.hasPermission(executingMember, interaction.channelId, "can_open_mod_menu")) - ) { - await interaction - .followUp({ content: "Error: Insufficient Permissions" }) - .catch((err) => logger.error(`Mod menu interaction follow up failed: ${err}`)); - return; - } - - const user = await resolveUser(pluginData.client, interaction.targetId); - if (!user.id) { - await interaction - .followUp("Error: User not found") - .catch((err) => logger.error(`Mod menu interaction follow up failed: ${err}`)); - return; - } - - // Load cases and display mod menu - const cases: Case[] = await pluginData.state.cases.with("notes").getByUserId(user.id); - const userName = - user instanceof UnknownUser && cases.length ? cases[cases.length - 1].user_name : renderUserUsername(user); - const casesPlugin = pluginData.getPlugin(CasesPlugin); - const totalCases = cases.length; - const totalPages: number = Math.max(Math.ceil(totalCases / CASES_PER_PAGE), 1); - const prefix = getGuildPrefix(pluginData); - const infoEmbed = await getUserInfoEmbed(pluginData, user.id, false); - displayModMenu( - pluginData, - interaction, - totalPages, - async (page) => { - const pageCases: Case[] = await pluginData.state.cases - .with("notes") - .getRecentByUserId(user.id, CASES_PER_PAGE, (page - 1) * CASES_PER_PAGE); - const lines = await asyncMap(pageCases, (c) => casesPlugin.getCaseSummary(c, true, interaction.targetId)); - - const firstCaseNum = (page - 1) * CASES_PER_PAGE + 1; - const lastCaseNum = Math.min(page * CASES_PER_PAGE, totalCases); - const title = - lines.length == 0 - ? `${userName}` - : `Most recent cases for ${userName} | ${firstCaseNum}-${lastCaseNum} of ${totalCases}`; - - const embed = { - author: { - name: title, - icon_url: user instanceof User ? user.displayAvatarURL() : undefined, - }, - fields: [ - ...getChunkedEmbedFields( - emptyEmbedValue, - lines.length == 0 ? `No cases found for **${userName}**` : lines.join("\n"), - ), - { - name: emptyEmbedValue, - value: trimLines( - lines.length == 0 ? "" : `Use \`${prefix}case \` to see more information about an individual case`, - ), - }, - ], - footer: { text: `Page ${page}/${totalPages}` }, - } satisfies APIEmbed; - - return embed; - }, - infoEmbed, - executingMember, - ); - }, -}); - -async function displayModMenu( - pluginData: GuildPluginData, - interaction: ContextMenuCommandInteraction, - totalPages: number, - loadPage: LoadModMenuPageFn, - infoEmbed: APIEmbed | null, - executingMember: GuildMember, -) { - if (interaction.deferred == false) { - await interaction.deferReply().catch((err) => logger.error(`Mod menu interaction defer failed: ${err}`)); - } - - const firstButton = new ButtonBuilder() - .setStyle(ButtonStyle.Secondary) - .setEmoji("⏪") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.FIRST })) - .setDisabled(true); - const prevButton = new ButtonBuilder() - .setStyle(ButtonStyle.Secondary) - .setEmoji("⬅") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.PREV })) - .setDisabled(true); - const infoButton = new ButtonBuilder() - .setStyle(ButtonStyle.Primary) - .setLabel("Info") - .setEmoji("ℹ") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.INFO })) - .setDisabled(infoEmbed != null ? false : true); - const nextButton = new ButtonBuilder() - .setStyle(ButtonStyle.Secondary) - .setEmoji("➡") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.NEXT })) - .setDisabled(totalPages > 1 ? false : true); - const lastButton = new ButtonBuilder() - .setStyle(ButtonStyle.Secondary) - .setEmoji("⏩") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.LAST })) - .setDisabled(totalPages > 1 ? false : true); - const navigationButtons = [firstButton, prevButton, infoButton, nextButton, lastButton] satisfies ButtonBuilder[]; - - const modactions = pluginData.getPlugin(ModActionsPlugin); - const moderationButtons = [ - new ButtonBuilder() - .setStyle(ButtonStyle.Primary) - .setLabel("Note") - .setEmoji("📝") - .setDisabled(!(await modactions.hasNotePermission(executingMember, interaction.channelId))) - .setCustomId(serializeCustomId({ action: ModMenuActionType.NOTE, target: interaction.targetId })), - new ButtonBuilder() - .setStyle(ButtonStyle.Primary) - .setLabel("Warn") - .setEmoji("⚠️") - .setDisabled(!(await modactions.hasWarnPermission(executingMember, interaction.channelId))) - .setCustomId(serializeCustomId({ action: ModMenuActionType.WARN, target: interaction.targetId })), - new ButtonBuilder() - .setStyle(ButtonStyle.Primary) - .setLabel("Mute") - .setEmoji("🔇") - .setDisabled(!(await modactions.hasMutePermission(executingMember, interaction.channelId))) - .setCustomId(serializeCustomId({ action: ModMenuActionType.MUTE, target: interaction.targetId })), - new ButtonBuilder() - .setStyle(ButtonStyle.Primary) - .setLabel("Ban") - .setEmoji("🚫") - .setDisabled(!(await modactions.hasBanPermission(executingMember, interaction.channelId))) - .setCustomId(serializeCustomId({ action: ModMenuActionType.BAN, target: interaction.targetId })), - ] satisfies ButtonBuilder[]; - - const navigationRow = new ActionRowBuilder().addComponents(navigationButtons); - const moderationRow = new ActionRowBuilder().addComponents(moderationButtons); - - let page = 1; - await interaction - .editReply({ - embeds: [await loadPage(page)], - components: [navigationRow, moderationRow], - }) - .then(async (currentPage) => { - const collector = await currentPage.createMessageComponentCollector({ - time: MOD_MENU_TIMEOUT, - }); - - collector.on("collect", async (i) => { - const opts = deserializeCustomId(i.customId); - if (opts.action == ModMenuActionType.PAGE) { - await i.deferUpdate().catch((err) => logger.error(`Mod menu defer failed: ${err}`)); - } - - // Update displayed embed if any navigation buttons were used - if (opts.action == ModMenuActionType.PAGE && opts.target == ModMenuNavigationType.INFO && infoEmbed != null) { - infoButton - .setLabel("Cases") - .setEmoji("📋") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.CASES })); - firstButton.setDisabled(true); - prevButton.setDisabled(true); - nextButton.setDisabled(true); - lastButton.setDisabled(true); - - await i - .editReply({ - embeds: [infoEmbed], - components: [navigationRow, moderationRow], - }) - .catch((err) => logger.error(`Mod menu info view failed: ${err}`)); - } else if (opts.action == ModMenuActionType.PAGE && opts.target == ModMenuNavigationType.CASES) { - infoButton - .setLabel("Info") - .setEmoji("ℹ") - .setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.INFO })); - updateNavButtonState(firstButton, prevButton, nextButton, lastButton, page, totalPages); - - await i - .editReply({ - embeds: [await loadPage(page)], - components: [navigationRow, moderationRow], - }) - .catch((err) => logger.error(`Mod menu cases view failed: ${err}`)); - } else if (opts.action == ModMenuActionType.PAGE) { - let pageDelta = 0; - switch (opts.target) { - case ModMenuNavigationType.PREV: - pageDelta = -1; - break; - case ModMenuNavigationType.NEXT: - pageDelta = 1; - break; - } - - let newPage = 1; - if (opts.target == ModMenuNavigationType.PREV || opts.target == ModMenuNavigationType.NEXT) { - newPage = Math.max(Math.min(page + pageDelta, totalPages), 1); - } else if (opts.target == ModMenuNavigationType.FIRST) { - newPage = 1; - } else if (opts.target == ModMenuNavigationType.LAST) { - newPage = totalPages; - } - - if (newPage != page) { - updateNavButtonState(firstButton, prevButton, nextButton, lastButton, newPage, totalPages); - - await i - .editReply({ - embeds: [await loadPage(newPage)], - components: [navigationRow, moderationRow], - }) - .catch((err) => logger.error(`Mod menu navigation failed: ${err}`)); - - page = newPage; - } - } else if (opts.action == ModMenuActionType.NOTE) { - await launchNoteActionModal(pluginData, i as ButtonInteraction, opts.target); - } else if (opts.action == ModMenuActionType.WARN) { - await launchWarnActionModal(pluginData, i as ButtonInteraction, opts.target); - } else if (opts.action == ModMenuActionType.MUTE) { - await launchMuteActionModal(pluginData, i as ButtonInteraction, opts.target); - } else if (opts.action == ModMenuActionType.BAN) { - await launchBanActionModal(pluginData, i as ButtonInteraction, opts.target); - } - - collector.resetTimer(); - }); - - // Remove components on timeout. - collector.on("end", async (_, reason) => { - if (reason !== "messageDelete") { - await interaction - .editReply({ - components: [], - }) - .catch((err) => logger.error(`Mod menu timeout failed: ${err}`)); - } - }); - }) - .catch((err) => logger.error(`Mod menu setup failed: ${err}`)); -} - -function serializeCustomId(opts: ModMenuActionOpts) { - return `${opts.action}:${opts.target}`; -} - -function deserializeCustomId(customId: string): ModMenuActionOpts { - const opts: ModMenuActionOpts = { - action: customId.split(":")[0] as ModMenuActionType, - target: customId.split(":")[1], - }; - - return opts; -} - -function updateNavButtonState( - firstButton: ButtonBuilder, - prevButton: ButtonBuilder, - nextButton: ButtonBuilder, - lastButton: ButtonBuilder, - currentPage: number, - totalPages: number, -) { - if (currentPage > 1) { - firstButton.setDisabled(false); - prevButton.setDisabled(false); - } else { - firstButton.setDisabled(true); - prevButton.setDisabled(true); - } - - if (currentPage == totalPages) { - nextButton.setDisabled(true); - lastButton.setDisabled(true); - } else { - nextButton.setDisabled(false); - lastButton.setDisabled(false); - } -} diff --git a/backend/src/plugins/ContextMenus/commands/MuteUserCtxCmd.ts b/backend/src/plugins/ContextMenus/commands/MuteUserCtxCmd.ts deleted file mode 100644 index 55123597..00000000 --- a/backend/src/plugins/ContextMenus/commands/MuteUserCtxCmd.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PermissionFlagsBits } from "discord.js"; -import { guildPluginUserContextMenuCommand } from "knub"; -import { launchMuteActionModal } from "../actions/mute"; - -export const MuteCmd = guildPluginUserContextMenuCommand({ - name: "Mute", - defaultMemberPermissions: PermissionFlagsBits.ModerateMembers.toString(), - async run({ pluginData, interaction }) { - await launchMuteActionModal(pluginData, interaction, interaction.targetId); - }, -}); diff --git a/backend/src/plugins/ContextMenus/commands/NoteUserCtxCmd.ts b/backend/src/plugins/ContextMenus/commands/NoteUserCtxCmd.ts deleted file mode 100644 index d1163383..00000000 --- a/backend/src/plugins/ContextMenus/commands/NoteUserCtxCmd.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PermissionFlagsBits } from "discord.js"; -import { guildPluginUserContextMenuCommand } from "knub"; -import { launchNoteActionModal } from "../actions/note"; - -export const NoteCmd = guildPluginUserContextMenuCommand({ - name: "Note", - defaultMemberPermissions: PermissionFlagsBits.ManageMessages.toString(), - async run({ pluginData, interaction }) { - await launchNoteActionModal(pluginData, interaction, interaction.targetId); - }, -}); diff --git a/backend/src/plugins/ContextMenus/commands/WarnUserCtxCmd.ts b/backend/src/plugins/ContextMenus/commands/WarnUserCtxCmd.ts deleted file mode 100644 index b3e6a545..00000000 --- a/backend/src/plugins/ContextMenus/commands/WarnUserCtxCmd.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PermissionFlagsBits } from "discord.js"; -import { guildPluginUserContextMenuCommand } from "knub"; -import { launchWarnActionModal } from "../actions/warn"; - -export const WarnCmd = guildPluginUserContextMenuCommand({ - name: "Warn", - defaultMemberPermissions: PermissionFlagsBits.ManageMessages.toString(), - async run({ pluginData, interaction }) { - await launchWarnActionModal(pluginData, interaction, interaction.targetId); - }, -}); diff --git a/backend/src/plugins/ContextMenus/events/ContextClickedEvt.ts b/backend/src/plugins/ContextMenus/events/ContextClickedEvt.ts new file mode 100644 index 00000000..98e6ab1f --- /dev/null +++ b/backend/src/plugins/ContextMenus/events/ContextClickedEvt.ts @@ -0,0 +1,12 @@ +import { contextMenuEvt } from "../types"; +import { routeContextAction } from "../utils/contextRouter"; + +export const ContextClickedEvt = contextMenuEvt({ + event: "interactionCreate", + + async listener(meta) { + if (!meta.args.interaction.isContextMenuCommand()) return; + const inter = meta.args.interaction; + await routeContextAction(meta.pluginData, inter); + }, +}); diff --git a/backend/src/plugins/ContextMenus/types.ts b/backend/src/plugins/ContextMenus/types.ts index a66ead4b..5dbcd9ce 100644 --- a/backend/src/plugins/ContextMenus/types.ts +++ b/backend/src/plugins/ContextMenus/types.ts @@ -1,40 +1,23 @@ -import { BasePluginType } from "knub"; -import { GuildCases } from "src/data/GuildCases"; +import { BasePluginType, guildPluginEventListener } from "knub"; import z from "zod"; +import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks"; export const zContextMenusConfig = z.strictObject({ can_use: z.boolean(), - can_open_mod_menu: z.boolean(), + user_muteindef: z.boolean(), + user_mute1d: z.boolean(), + user_mute1h: z.boolean(), + user_info: z.boolean(), + message_clean10: z.boolean(), + message_clean25: z.boolean(), + message_clean50: z.boolean(), }); export interface ContextMenuPluginType extends BasePluginType { config: z.infer; state: { - cases: GuildCases; + contextMenuLinks: GuildContextMenuLinks; }; } -export const enum ModMenuActionType { - PAGE = "page", - NOTE = "note", - WARN = "warn", - CLEAN = "clean", - MUTE = "mute", - BAN = "ban", -} - -export const enum ModMenuNavigationType { - FIRST = "first", - PREV = "prev", - NEXT = "next", - LAST = "last", - INFO = "info", - CASES = "cases", -} - -export interface ModMenuActionOpts { - action: ModMenuActionType; - target: string; -} - -export type LoadModMenuPageFn = (page: number) => Awaitable; +export const contextMenuEvt = guildPluginEventListener(); diff --git a/backend/src/plugins/ContextMenus/utils/contextRouter.ts b/backend/src/plugins/ContextMenus/utils/contextRouter.ts new file mode 100644 index 00000000..18b7b064 --- /dev/null +++ b/backend/src/plugins/ContextMenus/utils/contextRouter.ts @@ -0,0 +1,13 @@ +import { ContextMenuCommandInteraction } from "discord.js"; +import { GuildPluginData } from "knub"; +import { ContextMenuPluginType } from "../types"; +import { hardcodedActions } from "./hardcodedContextOptions"; + +export async function routeContextAction( + pluginData: GuildPluginData, + interaction: ContextMenuCommandInteraction, +) { + const contextLink = await pluginData.state.contextMenuLinks.get(interaction.commandId); + if (!contextLink) return; + hardcodedActions[contextLink.action_name](pluginData, interaction); +} diff --git a/backend/src/plugins/ContextMenus/utils/hardcodedContextOptions.ts b/backend/src/plugins/ContextMenus/utils/hardcodedContextOptions.ts new file mode 100644 index 00000000..84593ace --- /dev/null +++ b/backend/src/plugins/ContextMenus/utils/hardcodedContextOptions.ts @@ -0,0 +1,23 @@ +import { cleanAction } from "../actions/clean"; +import { muteAction } from "../actions/mute"; +import { userInfoAction } from "../actions/userInfo"; + +export const hardcodedContext: Record = { + user_muteindef: "Mute Indefinitely", + user_mute1d: "Mute for 1 day", + user_mute1h: "Mute for 1 hour", + user_info: "Get Info", + message_clean10: "Clean 10 messages", + message_clean25: "Clean 25 messages", + message_clean50: "Clean 50 messages", +}; + +export const hardcodedActions = { + user_muteindef: (pluginData, interaction) => muteAction(pluginData, undefined, interaction), + user_mute1d: (pluginData, interaction) => muteAction(pluginData, "1d", interaction), + user_mute1h: (pluginData, interaction) => muteAction(pluginData, "1h", interaction), + user_info: (pluginData, interaction) => userInfoAction(pluginData, interaction), + message_clean10: (pluginData, interaction) => cleanAction(pluginData, 10, interaction), + message_clean25: (pluginData, interaction) => cleanAction(pluginData, 25, interaction), + message_clean50: (pluginData, interaction) => cleanAction(pluginData, 50, interaction), +}; diff --git a/backend/src/plugins/ContextMenus/utils/loadAllCommands.ts b/backend/src/plugins/ContextMenus/utils/loadAllCommands.ts new file mode 100644 index 00000000..97d73dfb --- /dev/null +++ b/backend/src/plugins/ContextMenus/utils/loadAllCommands.ts @@ -0,0 +1,39 @@ +import { ApplicationCommandData, ApplicationCommandType } from "discord.js"; +import { GuildPluginData } from "knub"; +import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin"; +import { ContextMenuPluginType } from "../types"; +import { hardcodedContext } from "./hardcodedContextOptions"; + +export async function loadAllCommands(pluginData: GuildPluginData) { + const comms = await pluginData.client.application!.commands; + const cfg = pluginData.config.get(); + const newCommands: ApplicationCommandData[] = []; + const addedNames: string[] = []; + + for (const [name, label] of Object.entries(hardcodedContext)) { + if (!cfg[name]) continue; + + const type = name.startsWith("user") ? ApplicationCommandType.User : ApplicationCommandType.Message; + const data: ApplicationCommandData = { + type, + name: label, + }; + + addedNames.push(name); + newCommands.push(data); + } + + const setCommands = await comms.set(newCommands, pluginData.guild.id).catch((e) => { + pluginData.getPlugin(LogsPlugin).logBotAlert({ body: `Unable to overwrite context menus: ${e}` }); + return undefined; + }); + if (!setCommands) return; + + const setCommandsArray = [...setCommands.values()]; + await pluginData.state.contextMenuLinks.deleteAll(); + + for (let i = 0; i < setCommandsArray.length; i++) { + const command = setCommandsArray[i]; + pluginData.state.contextMenuLinks.create(command.id, addedNames[i]); + } +} diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index 43655cc8..bc8c957b 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -173,7 +173,7 @@ export async function banUserId( mod, user, caseNumber: createdCase.case_number, - reason, + reason: reason ?? "", banTime: humanizeDuration(banTime), }); } else { diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index 4a67dbbe..d5314c0f 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -12,7 +12,7 @@ import { UserNotificationMethod, UserNotificationResult } from "../../utils"; import { CaseArgs } from "../Cases/types"; export const zModActionsConfig = z.strictObject({ - main_guild: tNullable(t.string), + main_guild: z.nullable(z.string()), dm_on_warn: z.boolean(), dm_on_kick: z.boolean(), dm_on_ban: z.boolean(), @@ -24,7 +24,7 @@ export const zModActionsConfig = z.strictObject({ kick_message: z.nullable(z.string()), ban_message: z.nullable(z.string()), tempban_message: z.nullable(z.string()), - default_ban_reason: tNullable(t.string), + default_ban_reason: z.nullable(z.string()), alert_on_rejoin: z.boolean(), alert_channel: z.nullable(z.string()), warn_notify_enabled: z.boolean(),