diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 24899916..7ae53217 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -3,6 +3,7 @@ */ import { + ChatInputCommandInteraction, GuildMember, Message, MessageCreateOptions, @@ -100,12 +101,19 @@ export function makeIoTsConfigParser>(schema: Schema) }; } +function isContextInteraction( + context: TextBasedChannel | ChatInputCommandInteraction, +): context is ChatInputCommandInteraction { + return "commandId" in context && !!context.commandId; +} + export async function sendSuccessMessage( pluginData: AnyPluginData, - channel: TextBasedChannel, + context: TextBasedChannel | ChatInputCommandInteraction, body: string, allowedMentions?: MessageMentionOptions, responseInteraction?: ModalSubmitInteraction, + ephemeral = false, ): Promise { const emoji = pluginData.fullConfig.success_emoji || undefined; const formattedBody = successMessage(body, emoji); @@ -117,23 +125,44 @@ export async function sendSuccessMessage( await responseInteraction .editReply({ content: formattedBody, embeds: [], components: [] }) .catch((err) => logger.error(`Interaction reply failed: ${err}`)); - } else { - return channel + + return; + } + + if (!isContextInteraction(context)) { + // noinspection TypeScriptValidateJSTypes + return context .send({ ...content }) // Force line break .catch((err) => { - const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id; + const channelInfo = "guild" in context ? `${context.id} (${context.guild.id})` : context.id; logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`); + return undefined; }); } + + const replyMethod = context.replied ? "followUp" : "reply"; + + return context[replyMethod]({ + content: formattedBody, + embeds: [], + components: [], + fetchReply: true, + ephemeral, + }).catch((err) => { + logger.error(`Context reply failed: ${err}`); + + return undefined; + }); } export async function sendErrorMessage( pluginData: AnyPluginData, - channel: TextBasedChannel, + context: TextBasedChannel | ChatInputCommandInteraction, body: string, allowedMentions?: MessageMentionOptions, responseInteraction?: ModalSubmitInteraction, + ephemeral = false, ): Promise { const emoji = pluginData.fullConfig.error_emoji || undefined; const formattedBody = errorMessage(body, emoji); @@ -145,15 +174,34 @@ export async function sendErrorMessage( await responseInteraction .editReply({ content: formattedBody, embeds: [], components: [] }) .catch((err) => logger.error(`Interaction reply failed: ${err}`)); - } else { - return channel + + return; + } + + if (!isContextInteraction(context)) { + // noinspection TypeScriptValidateJSTypes + return context .send({ ...content }) // Force line break .catch((err) => { - const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id; + const channelInfo = "guild" in context ? `${context.id} (${context.guild.id})` : context.id; logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`); return undefined; }); } + + const replyMethod = context.replied ? "followUp" : "reply"; + + return context[replyMethod]({ + content: formattedBody, + embeds: [], + components: [], + fetchReply: true, + ephemeral, + }).catch((err) => { + logger.error(`Context reply failed: ${err}`); + + return undefined; + }); } export function getBaseUrl(pluginData: AnyPluginData) { diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index 9b7d3404..57b2709e 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -28,13 +28,14 @@ import { MassbanCmd } from "./commands/MassBanCmd"; import { MassunbanCmd } from "./commands/MassUnbanCmd"; import { MassmuteCmd } from "./commands/MassmuteCmd"; import { MuteCmd } from "./commands/MuteCmd"; -import { NoteCmd } from "./commands/NoteCmd"; import { SoftbanCmd } from "./commands/SoftbanCommand"; import { UnbanCmd } from "./commands/UnbanCmd"; import { UnhideCaseCmd } from "./commands/UnhideCaseCmd"; import { UnmuteCmd } from "./commands/UnmuteCmd"; import { UpdateCmd } from "./commands/UpdateCmd"; import { WarnCmd } from "./commands/WarnCmd"; +import { NoteMsgCmd } from "./commands/note/NoteMsgCmd"; +import { NoteSlashCmd } from "./commands/note/NoteSlashCmd"; import { AuditLogEvents } from "./events/AuditLogEvents"; import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt"; import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt"; @@ -52,7 +53,14 @@ import { offModActionsEvent } from "./functions/offModActionsEvent"; import { onModActionsEvent } from "./functions/onModActionsEvent"; import { updateCase } from "./functions/updateCase"; import { warnMember } from "./functions/warnMember"; -import { BanOptions, ConfigSchema, KickOptions, ModActionsPluginType, WarnOptions } from "./types"; +import { + BanOptions, + ConfigSchema, + KickOptions, + ModActionsPluginType, + WarnOptions, + modActionsSlashGroup, +} from "./types"; const defaultOptions = { config: { @@ -135,9 +143,18 @@ export const ModActionsPlugin = zeppelinGuildPlugin()({ events: [CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, PostAlertOnMemberJoinEvt, AuditLogEvents], + slashCommands: [ + modActionsSlashGroup({ + name: "mod", + description: "Moderation actions", + defaultMemberPermissions: "0", + subcommands: [{ type: "slash", ...NoteSlashCmd }], + }), + ], + messageCommands: [ UpdateCmd, - NoteCmd, + NoteMsgCmd, WarnCmd, MuteCmd, ForcemuteCmd, diff --git a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts index 43575463..dc8df97f 100644 --- a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts @@ -6,13 +6,13 @@ import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from ". import { renderUserUsername, resolveMember, resolveUser } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), }; -export const AddCaseCmd = modActionsCmd({ +export const AddCaseCmd = modActionsMsgCmd({ trigger: "addcase", permission: "can_addcase", description: "Add an arbitrary case to the specified user without taking any action", diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts index 9d32cd10..f0a20d5f 100644 --- a/backend/src/plugins/ModActions/commands/BanCmd.ts +++ b/backend/src/plugins/ModActions/commands/BanCmd.ts @@ -13,7 +13,7 @@ import { banUserId } from "../functions/banUserId"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { isBanned } from "../functions/isBanned"; import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), @@ -22,7 +22,7 @@ const opts = { "delete-days": ct.number({ option: true, shortcut: "d" }), }; -export const BanCmd = modActionsCmd({ +export const BanCmd = modActionsMsgCmd({ trigger: "ban", permission: "can_ban", description: "Ban or Tempban the specified member", @@ -45,13 +45,12 @@ export const BanCmd = modActionsCmd({ async run({ pluginData, message: msg, args }) { const user = await resolveUser(pluginData.client, args.user); + if (!user.id) { sendErrorMessage(pluginData, msg.channel, `User not found`); return; } - const time = args["time"] ? args["time"] : null; - const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id); // The moderator who did the action is the message author or, if used, the specified -mod let mod = msg.member; @@ -64,85 +63,25 @@ export const BanCmd = modActionsCmd({ mod = args.mod; } + const time = args["time"] ? args["time"] : null; + const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); + // acquire a lock because of the needed user-inputs below (if banned/not on server) const lock = await pluginData.locks.acquire(banLock(user)); let forceban = false; const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id); + if (!memberToBan) { const banned = await isBanned(pluginData, user.id); - if (banned) { - // Abort if trying to ban user indefinitely if they are already banned indefinitely - if (!existingTempban && !time) { - sendErrorMessage(pluginData, msg.channel, `User is already banned indefinitely.`); - return; - } - // Ask the mod if we should update the existing ban - const reply = await waitForButtonConfirm( - msg.channel, - { content: "Failed to message the user. Log the warning anyway?" }, - { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, - ); - if (!reply) { - sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator"); - lock.unlock(); - return; - } else { - // Update or add new tempban / remove old tempban - if (time && time > 0) { - if (existingTempban) { - await pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id); - } else { - await pluginData.state.tempbans.addTempban(user.id, time, mod.id); - } - const tempban = (await pluginData.state.tempbans.findExistingTempbanForUserId(user.id))!; - registerExpiringTempban(tempban); - } else if (existingTempban) { - clearExpiringTempban(existingTempban); - pluginData.state.tempbans.clear(user.id); - } - - // Create a new case for the updated ban since we never stored the old case id and log the action - const casesPlugin = pluginData.getPlugin(CasesPlugin); - const createdCase = await casesPlugin.createCase({ - modId: mod.id, - type: CaseTypes.Ban, - userId: user.id, - reason, - noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`], - }); - if (time) { - pluginData.getPlugin(LogsPlugin).logMemberTimedBan({ - mod: mod.user, - user, - caseNumber: createdCase.case_number, - reason, - banTime: humanizeDuration(time), - }); - } else { - pluginData.getPlugin(LogsPlugin).logMemberBan({ - mod: mod.user, - user, - caseNumber: createdCase.case_number, - reason, - }); - } - - sendSuccessMessage( - pluginData, - msg.channel, - `Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`, - ); - lock.unlock(); - return; - } - } else { + if (!banned) { // Ask the mod if we should upgrade to a forceban as the user is not on the server const reply = await waitForButtonConfirm( msg.channel, { content: "User not on server, forceban instead?" }, { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, ); + if (!reply) { sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator"); lock.unlock(); @@ -151,6 +90,73 @@ export const BanCmd = modActionsCmd({ forceban = true; } } + + // Abort if trying to ban user indefinitely if they are already banned indefinitely + if (!existingTempban && !time) { + sendErrorMessage(pluginData, msg.channel, `User is already banned indefinitely.`); + return; + } + + // Ask the mod if we should update the existing ban + const reply = await waitForButtonConfirm( + msg.channel, + { content: "Failed to message the user. Log the warning anyway?" }, + { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + ); + + if (!reply) { + sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator"); + lock.unlock(); + return; + } + + // Update or add new tempban / remove old tempban + if (time && time > 0) { + if (existingTempban) { + await pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id); + } else { + await pluginData.state.tempbans.addTempban(user.id, time, mod.id); + } + const tempban = (await pluginData.state.tempbans.findExistingTempbanForUserId(user.id))!; + registerExpiringTempban(tempban); + } else if (existingTempban) { + clearExpiringTempban(existingTempban); + pluginData.state.tempbans.clear(user.id); + } + + // Create a new case for the updated ban since we never stored the old case id and log the action + const casesPlugin = pluginData.getPlugin(CasesPlugin); + const createdCase = await casesPlugin.createCase({ + modId: mod.id, + type: CaseTypes.Ban, + userId: user.id, + reason, + noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`], + }); + if (time) { + pluginData.getPlugin(LogsPlugin).logMemberTimedBan({ + mod: mod.user, + user, + caseNumber: createdCase.case_number, + reason, + banTime: humanizeDuration(time), + }); + } else { + pluginData.getPlugin(LogsPlugin).logMemberBan({ + mod: mod.user, + user, + caseNumber: createdCase.case_number, + reason, + }); + } + + sendSuccessMessage( + pluginData, + msg.channel, + `Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`, + ); + lock.unlock(); + return; } // Make sure we're allowed to ban this member if they are on the server diff --git a/backend/src/plugins/ModActions/commands/CaseCmd.ts b/backend/src/plugins/ModActions/commands/CaseCmd.ts index d3d8ce95..8a92ed3f 100644 --- a/backend/src/plugins/ModActions/commands/CaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/CaseCmd.ts @@ -1,9 +1,9 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { sendErrorMessage } from "../../../pluginUtils"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const CaseCmd = modActionsCmd({ +export const CaseCmd = modActionsMsgCmd({ trigger: "case", permission: "can_view", description: "Show information about a specific case", diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 5b0e3273..e07176f5 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -7,7 +7,7 @@ import { createPaginatedMessage } from "../../../utils/createPaginatedMessage"; import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; import { CasesPlugin } from "../../Cases/CasesPlugin"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.userId({ option: true }), @@ -15,7 +15,7 @@ const opts = { const casesPerPage = 5; -export const CasesModCmd = modActionsCmd({ +export const CasesModCmd = modActionsMsgCmd({ trigger: ["cases", "modlogs", "infractions"], permission: "can_view", description: "Show the most recent 5 cases by the specified -mod", diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 069ad31f..a92ea75d 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -7,7 +7,7 @@ import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUs import { asyncMap } from "../../../utils/async"; import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { expand: ct.bool({ option: true, isSwitch: true, shortcut: "e" }), @@ -21,7 +21,7 @@ const opts = { unbans: ct.switchOption({ def: false, shortcut: "ub" }), }; -export const CasesUserCmd = modActionsCmd({ +export const CasesUserCmd = modActionsMsgCmd({ trigger: ["cases", "modlogs"], permission: "can_view", description: "Show a list of cases the specified user has", diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts index 823bc726..cb3c56d8 100644 --- a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts @@ -6,9 +6,9 @@ import { SECONDS, trimLines } from "../../../utils"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const DeleteCaseCmd = modActionsCmd({ +export const DeleteCaseCmd = modActionsMsgCmd({ trigger: ["delete_case", "deletecase"], permission: "can_deletecase", description: trimLines(` diff --git a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts index 4ddef154..251be232 100644 --- a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts +++ b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts @@ -9,13 +9,13 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; import { isBanned } from "../functions/isBanned"; -import { IgnoredEventType, modActionsCmd } from "../types"; +import { IgnoredEventType, modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), }; -export const ForcebanCmd = modActionsCmd({ +export const ForcebanCmd = modActionsMsgCmd({ trigger: "forceban", permission: "can_ban", description: "Force-ban the specified user, even if they aren't on the server", diff --git a/backend/src/plugins/ModActions/commands/ForcemuteCmd.ts b/backend/src/plugins/ModActions/commands/ForcemuteCmd.ts index 18fe1228..4b6f90da 100644 --- a/backend/src/plugins/ModActions/commands/ForcemuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/ForcemuteCmd.ts @@ -2,7 +2,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { canActOn, sendErrorMessage } from "../../../pluginUtils"; import { resolveMember, resolveUser } from "../../../utils"; import { actualMuteUserCmd } from "../functions/actualMuteUserCmd"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), @@ -10,7 +10,7 @@ const opts = { "notify-channel": ct.textChannel({ option: true }), }; -export const ForcemuteCmd = modActionsCmd({ +export const ForcemuteCmd = modActionsMsgCmd({ trigger: "forcemute", permission: "can_mute", description: "Force-mute the specified user, even if they're not on the server", diff --git a/backend/src/plugins/ModActions/commands/ForceunmuteCmd.ts b/backend/src/plugins/ModActions/commands/ForceunmuteCmd.ts index 8ce0ce14..5ce489a1 100644 --- a/backend/src/plugins/ModActions/commands/ForceunmuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/ForceunmuteCmd.ts @@ -2,13 +2,13 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { canActOn, sendErrorMessage } from "../../../pluginUtils"; import { resolveMember, resolveUser } from "../../../utils"; import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), }; -export const ForceUnmuteCmd = modActionsCmd({ +export const ForceUnmuteCmd = modActionsMsgCmd({ trigger: "forceunmute", permission: "can_mute", description: "Force-unmute the specified user, even if they're not on the server", diff --git a/backend/src/plugins/ModActions/commands/HideCaseCmd.ts b/backend/src/plugins/ModActions/commands/HideCaseCmd.ts index 38337d85..b08756db 100644 --- a/backend/src/plugins/ModActions/commands/HideCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/HideCaseCmd.ts @@ -1,8 +1,8 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const HideCaseCmd = modActionsCmd({ +export const HideCaseCmd = modActionsMsgCmd({ trigger: ["hide", "hidecase", "hide_case"], permission: "can_hidecase", description: "Hide the specified case so it doesn't appear in !cases or !info", diff --git a/backend/src/plugins/ModActions/commands/KickCmd.ts b/backend/src/plugins/ModActions/commands/KickCmd.ts index 080294d0..2e32f3e2 100644 --- a/backend/src/plugins/ModActions/commands/KickCmd.ts +++ b/backend/src/plugins/ModActions/commands/KickCmd.ts @@ -1,6 +1,6 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { actualKickMemberCmd } from "../functions/actualKickMemberCmd"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), @@ -9,7 +9,7 @@ const opts = { clean: ct.bool({ option: true, isSwitch: true }), }; -export const KickCmd = modActionsCmd({ +export const KickCmd = modActionsMsgCmd({ trigger: "kick", permission: "can_kick", description: "Kick the specified member", diff --git a/backend/src/plugins/ModActions/commands/MassBanCmd.ts b/backend/src/plugins/ModActions/commands/MassBanCmd.ts index d31aadd7..4769cd51 100644 --- a/backend/src/plugins/ModActions/commands/MassBanCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassBanCmd.ts @@ -11,9 +11,9 @@ import { DAYS, MINUTES, SECONDS, noop } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; -import { IgnoredEventType, modActionsCmd } from "../types"; +import { IgnoredEventType, modActionsMsgCmd } from "../types"; -export const MassbanCmd = modActionsCmd({ +export const MassbanCmd = modActionsMsgCmd({ trigger: "massban", permission: "can_massban", description: "Mass-ban a list of user IDs", diff --git a/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts b/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts index c5ce37ac..a873b4c6 100644 --- a/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassUnbanCmd.ts @@ -9,9 +9,9 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; import { isBanned } from "../functions/isBanned"; -import { IgnoredEventType, modActionsCmd } from "../types"; +import { IgnoredEventType, modActionsMsgCmd } from "../types"; -export const MassunbanCmd = modActionsCmd({ +export const MassunbanCmd = modActionsMsgCmd({ trigger: "massunban", permission: "can_massunban", description: "Mass-unban a list of user IDs", diff --git a/backend/src/plugins/ModActions/commands/MassmuteCmd.ts b/backend/src/plugins/ModActions/commands/MassmuteCmd.ts index a62c029e..65422f0f 100644 --- a/backend/src/plugins/ModActions/commands/MassmuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassmuteCmd.ts @@ -7,9 +7,9 @@ import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginU import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const MassmuteCmd = modActionsCmd({ +export const MassmuteCmd = modActionsMsgCmd({ trigger: "massmute", permission: "can_massmute", description: "Mass-mute a list of user IDs", diff --git a/backend/src/plugins/ModActions/commands/MuteCmd.ts b/backend/src/plugins/ModActions/commands/MuteCmd.ts index f824fca3..4c505701 100644 --- a/backend/src/plugins/ModActions/commands/MuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/MuteCmd.ts @@ -4,7 +4,7 @@ import { resolveMember, resolveUser } from "../../../utils"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; import { actualMuteUserCmd } from "../functions/actualMuteUserCmd"; import { isBanned } from "../functions/isBanned"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), @@ -12,7 +12,7 @@ const opts = { "notify-channel": ct.textChannel({ option: true }), }; -export const MuteCmd = modActionsCmd({ +export const MuteCmd = modActionsMsgCmd({ trigger: "mute", permission: "can_mute", description: "Mute the specified member", diff --git a/backend/src/plugins/ModActions/commands/NoteCmd.ts b/backend/src/plugins/ModActions/commands/NoteCmd.ts deleted file mode 100644 index b13ed498..00000000 --- a/backend/src/plugins/ModActions/commands/NoteCmd.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { CaseTypes } from "../../../data/CaseTypes"; -import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername, resolveUser } from "../../../utils"; -import { CasesPlugin } from "../../Cases/CasesPlugin"; -import { LogsPlugin } from "../../Logs/LogsPlugin"; -import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; -import { modActionsCmd } from "../types"; - -export const NoteCmd = modActionsCmd({ - trigger: "note", - permission: "can_note", - description: "Add a note to the specified user", - - signature: { - user: ct.string(), - note: ct.string({ required: false, catchAll: true }), - }, - - async run({ pluginData, message: msg, args }) { - const user = await resolveUser(pluginData.client, args.user); - if (!user.id) { - sendErrorMessage(pluginData, msg.channel, `User not found`); - return; - } - - if (!args.note && msg.attachments.size === 0) { - sendErrorMessage(pluginData, msg.channel, "Text or attachment required"); - return; - } - - const userName = renderUserUsername(user); - const reason = formatReasonWithAttachments(args.note, [...msg.attachments.values()]); - - const casesPlugin = pluginData.getPlugin(CasesPlugin); - const createdCase = await casesPlugin.createCase({ - userId: user.id, - modId: msg.author.id, - type: CaseTypes.Note, - reason, - }); - - pluginData.getPlugin(LogsPlugin).logMemberNote({ - mod: msg.author, - user, - caseNumber: createdCase.case_number, - reason, - }); - - sendSuccessMessage(pluginData, msg.channel, `Note added on **${userName}** (Case #${createdCase.case_number})`); - - pluginData.state.events.emit("note", user.id, reason); - }, -}); diff --git a/backend/src/plugins/ModActions/commands/SoftbanCommand.ts b/backend/src/plugins/ModActions/commands/SoftbanCommand.ts index 9bbb6415..a5806cdd 100644 --- a/backend/src/plugins/ModActions/commands/SoftbanCommand.ts +++ b/backend/src/plugins/ModActions/commands/SoftbanCommand.ts @@ -1,7 +1,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { trimPluginDescription } from "../../../utils"; import { actualKickMemberCmd } from "../functions/actualKickMemberCmd"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), @@ -9,7 +9,7 @@ const opts = { "notify-channel": ct.textChannel({ option: true }), }; -export const SoftbanCmd = modActionsCmd({ +export const SoftbanCmd = modActionsMsgCmd({ trigger: "softban", permission: "can_kick", description: trimPluginDescription(` diff --git a/backend/src/plugins/ModActions/commands/UnbanCmd.ts b/backend/src/plugins/ModActions/commands/UnbanCmd.ts index 53363ee0..d232f4e4 100644 --- a/backend/src/plugins/ModActions/commands/UnbanCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnbanCmd.ts @@ -9,13 +9,13 @@ import { resolveUser } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { ignoreEvent } from "../functions/ignoreEvent"; -import { IgnoredEventType, modActionsCmd } from "../types"; +import { IgnoredEventType, modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), }; -export const UnbanCmd = modActionsCmd({ +export const UnbanCmd = modActionsMsgCmd({ trigger: "unban", permission: "can_unban", description: "Unban the specified member", diff --git a/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts b/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts index 9fe9e208..7c6eb774 100644 --- a/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts @@ -1,8 +1,8 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const UnhideCaseCmd = modActionsCmd({ +export const UnhideCaseCmd = modActionsMsgCmd({ trigger: ["unhide", "unhidecase", "unhide_case"], permission: "can_hidecase", description: "Un-hide the specified case, making it appear in !cases and !info again", diff --git a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts index 55c84631..3a4171ac 100644 --- a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts @@ -5,13 +5,13 @@ import { resolveMember, resolveUser } from "../../../utils"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd"; import { isBanned } from "../functions/isBanned"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; const opts = { mod: ct.member({ option: true }), }; -export const UnmuteCmd = modActionsCmd({ +export const UnmuteCmd = modActionsMsgCmd({ trigger: "unmute", permission: "can_mute", description: "Unmute the specified member", diff --git a/backend/src/plugins/ModActions/commands/UpdateCmd.ts b/backend/src/plugins/ModActions/commands/UpdateCmd.ts index 3310522e..59c68b0d 100644 --- a/backend/src/plugins/ModActions/commands/UpdateCmd.ts +++ b/backend/src/plugins/ModActions/commands/UpdateCmd.ts @@ -1,8 +1,8 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { updateCase } from "../functions/updateCase"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const UpdateCmd = modActionsCmd({ +export const UpdateCmd = modActionsMsgCmd({ trigger: ["update", "reason"], permission: "can_note", description: diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index c8192015..2b3121fd 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -8,9 +8,9 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach import { isBanned } from "../functions/isBanned"; import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs"; import { warnMember } from "../functions/warnMember"; -import { modActionsCmd } from "../types"; +import { modActionsMsgCmd } from "../types"; -export const WarnCmd = modActionsCmd({ +export const WarnCmd = modActionsMsgCmd({ trigger: "warn", permission: "can_warn", description: "Send a warning to the specified user", diff --git a/backend/src/plugins/ModActions/commands/note/NoteMsgCmd.ts b/backend/src/plugins/ModActions/commands/note/NoteMsgCmd.ts new file mode 100644 index 00000000..c7122fe9 --- /dev/null +++ b/backend/src/plugins/ModActions/commands/note/NoteMsgCmd.ts @@ -0,0 +1,31 @@ +import { commandTypeHelpers as ct } from "../../../../commandTypes"; +import { sendErrorMessage } from "../../../../pluginUtils"; +import { resolveUser } from "../../../../utils"; +import { actualNoteCmd } from "../../functions/actualNoteCmd"; +import { modActionsMsgCmd } from "../../types"; + +export const NoteMsgCmd = modActionsMsgCmd({ + trigger: "note", + permission: "can_note", + description: "Add a note to the specified user", + + signature: { + user: ct.string(), + note: ct.string({ required: false, catchAll: true }), + }, + + async run({ pluginData, message: msg, args }) { + const user = await resolveUser(pluginData.client, args.user); + if (!user.id) { + sendErrorMessage(pluginData, msg.channel, `User not found`); + return; + } + + if (!args.note && msg.attachments.size === 0) { + sendErrorMessage(pluginData, msg.channel, "Text or attachment required"); + return; + } + + actualNoteCmd(pluginData, msg.channel, msg.author, [...msg.attachments.values()], user, args.note || ""); + }, +}); diff --git a/backend/src/plugins/ModActions/commands/note/NoteSlashCmd.ts b/backend/src/plugins/ModActions/commands/note/NoteSlashCmd.ts new file mode 100644 index 00000000..82b38be7 --- /dev/null +++ b/backend/src/plugins/ModActions/commands/note/NoteSlashCmd.ts @@ -0,0 +1,45 @@ +import { ApplicationCommandOptionType, ChatInputCommandInteraction } from "discord.js"; +import { slashOptions } from "knub"; +import { sendErrorMessage } from "../../../../pluginUtils"; +import { actualNoteCmd } from "../../functions/actualNoteCmd"; + +export const NoteSlashCmd = { + name: "note", + description: "Add a note to the specified user", + allowDms: false, + configPermission: "can_note", + + signature: [ + slashOptions.user({ name: "user", description: "The user to add a note to", required: true }), + slashOptions.string({ name: "note", description: "The note to add to the user", required: false }), + ...new Array(10).fill(0).map((_, i) => { + return { + name: `attachment${i + 1}`, + description: "An attachment to add to the note", + type: ApplicationCommandOptionType.Attachment, + required: false, + resolveValue: (interaction: ChatInputCommandInteraction) => { + return interaction.options.getAttachment(`attachment${i + 1}`); + }, + getExtraAPIProps: () => ({}), + }; + }), + ], + + async run({ interaction, options, pluginData }) { + const attachments = new Array(10) + .fill(0) + .map((_, i) => { + return options[`attachment${i + 1}`]; + }) + .filter((a) => a); + + if ((!options.note || options.note.trim() === "") && attachments.length < 1) { + sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true); + + return; + } + + actualNoteCmd(pluginData, interaction, interaction.user, attachments, options.user, options.note || ""); + }, +}; diff --git a/backend/src/plugins/ModActions/functions/actualNoteCmd.ts b/backend/src/plugins/ModActions/functions/actualNoteCmd.ts new file mode 100644 index 00000000..524a182c --- /dev/null +++ b/backend/src/plugins/ModActions/functions/actualNoteCmd.ts @@ -0,0 +1,47 @@ +import { Attachment, ChatInputCommandInteraction, TextBasedChannel, User } from "discord.js"; +import { GuildPluginData } from "knub"; +import { CaseTypes } from "../../../data/CaseTypes"; +import { sendSuccessMessage } from "../../../pluginUtils"; +import { UnknownUser, renderUserUsername } from "../../../utils"; +import { CasesPlugin } from "../../Cases/CasesPlugin"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { ModActionsPluginType } from "../types"; +import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; + +export async function actualNoteCmd( + pluginData: GuildPluginData, + context: TextBasedChannel | ChatInputCommandInteraction, + author: User, + attachments: Array, + user: User | UnknownUser, + note: string, +) { + const userName = renderUserUsername(user); + const reason = formatReasonWithAttachments(note, attachments); + + const casesPlugin = pluginData.getPlugin(CasesPlugin); + const createdCase = await casesPlugin.createCase({ + userId: user.id, + modId: author.id, + type: CaseTypes.Note, + reason, + }); + + pluginData.getPlugin(LogsPlugin).logMemberNote({ + mod: author, + user, + caseNumber: createdCase.case_number, + reason, + }); + + sendSuccessMessage( + pluginData, + context, + `Note added on **${userName}** (Case #${createdCase.case_number})`, + undefined, + undefined, + true, + ); + + pluginData.state.events.emit("note", user.id, reason); +} diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index 447b9638..c6c1fe70 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -1,7 +1,7 @@ import { GuildTextBasedChannel } from "discord.js"; import { EventEmitter } from "events"; import * as t from "io-ts"; -import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, guildPluginSlashGroup } from "knub"; import { Queue } from "../../Queue"; import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; @@ -147,5 +147,6 @@ export interface BanOptions { export type ModActionType = "note" | "warn" | "mute" | "unmute" | "kick" | "ban" | "unban"; -export const modActionsCmd = guildPluginMessageCommand(); +export const modActionsMsgCmd = guildPluginMessageCommand(); +export const modActionsSlashGroup = guildPluginSlashGroup(); export const modActionsEvt = guildPluginEventListener(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 3df9c347..45119f0f 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -53,8 +53,8 @@ export const guildPlugins: Array> = [ PostPlugin, ReactionRolesPlugin, MessageSaverPlugin, - // GuildMemberCachePlugin, // FIXME: New caching thing, or fix deadlocks with this plugin ModActionsPlugin, + // GuildMemberCachePlugin, // FIXME: New caching thing, or fix deadlocks with this plugin NameHistoryPlugin, RemindersPlugin, RolesPlugin,