diff --git a/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts b/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts index ee4a663a..d9952d48 100644 --- a/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts +++ b/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts @@ -1,38 +1,34 @@ import { PluginOptions } from "knub"; -import { StrictValidationError } from "src/validatorUtils"; -import { ConfigPreprocessorFn } from "knub/dist/config/configTypes"; import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; -import { availableTypes } from "./actions/availableActions"; import { ContextClickedEvt } from "./events/ContextClickedEvt"; import { ConfigSchema, ContextMenuPluginType } from "./types"; import { loadAllCommands } from "./utils/loadAllCommands"; +import { UtilityPlugin } from "../Utility/UtilityPlugin"; const defaultOptions: PluginOptions = { config: { - context_actions: {}, + can_use: false, + + user_muteindef: false, + user_mute1d: false, + user_mute1h: false, + user_info: false, + + message_clean10: false, + message_clean25: false, + message_clean50: false, }, -}; - -const configPreprocessor: ConfigPreprocessorFn = options => { - if (options.config.context_actions) { - for (const [name, contextMenu] of Object.entries(options.config.context_actions)) { - if (Object.entries(contextMenu.action).length !== 1) { - throw new StrictValidationError([`Invalid value for context_actions/${name}: Must have exactly one action.`]); - } - - const actionName = Object.entries(contextMenu.action)[0][0]; - if (!availableTypes[actionName].includes(contextMenu.type)) { - throw new StrictValidationError([ - `Invalid value for context_actions/${name}/${actionName}: ${actionName} is not allowed on type ${contextMenu.type}.`, - ]); - } - } - } - - return options; + overrides: [ + { + level: ">=50", + config: { + can_use: true, + }, + }, + ], }; export const ContextMenuPlugin = zeppelinGuildPlugin()({ @@ -40,7 +36,6 @@ export const ContextMenuPlugin = zeppelinGuildPlugin()({ configSchema: ConfigSchema, defaultOptions, - configPreprocessor, // prettier-ignore events: [ @@ -57,5 +52,5 @@ export const ContextMenuPlugin = zeppelinGuildPlugin()({ loadAllCommands(pluginData); }, - dependencies: [MutesPlugin, LogsPlugin], + dependencies: [MutesPlugin, LogsPlugin, UtilityPlugin], }); diff --git a/backend/src/plugins/ContextMenus/actions/availableActions.ts b/backend/src/plugins/ContextMenus/actions/availableActions.ts deleted file mode 100644 index 24816790..00000000 --- a/backend/src/plugins/ContextMenus/actions/availableActions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as t from "io-ts"; -import { ContextActionBlueprint } from "../helpers"; -import { CleanAction } from "./clean"; -import { MuteAction } from "./mute"; - -export const availableActions: Record> = { - mute: MuteAction, - clean: CleanAction, -}; - -export const AvailableActions = t.type({ - mute: MuteAction.configType, - clean: CleanAction.configType, -}); - -export const availableTypes: Record = { - mute: ["USER"], - clean: ["MESSAGE"], -}; diff --git a/backend/src/plugins/ContextMenus/actions/clean.ts b/backend/src/plugins/ContextMenus/actions/clean.ts index 8184d480..55ff9d4e 100644 --- a/backend/src/plugins/ContextMenus/actions/clean.ts +++ b/backend/src/plugins/ContextMenus/actions/clean.ts @@ -1,64 +1,52 @@ -import { TextChannel } from "discord.js"; -import * as t from "io-ts"; -import { canActOn } from "src/pluginUtils"; +import { ContextMenuInteraction, TextChannel } from "discord.js"; +import { GuildPluginData } from "knub"; import { LogType } from "../../../data/LogType"; import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { tNullable } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; -import { contextMenuAction } from "../helpers"; +import { ContextMenuPluginType } from "../types"; -export const CleanAction = contextMenuAction({ - configType: t.type({ - amount: tNullable(t.number), - targetUserOnly: tNullable(t.boolean), - "delete-pins": tNullable(t.boolean), - }), +export async function cleanAction( + pluginData: GuildPluginData, + amount: number, + interaction: ContextMenuInteraction, +) { + interaction.deferReply({ ephemeral: true }); + const executingMember = await pluginData.guild.members.fetch(interaction.user.id); + const userCfg = await pluginData.config.getMatchingConfig({ + channelId: interaction.channelId, + member: executingMember, + }); - defaultConfig: { - amount: 10, - targetUserOnly: false, - "delete-pins": false, - }, + // TODO: Add perm check for can_clean in util + if (!userCfg.can_use) { + await interaction.followUp({ content: "Cannot clean: insufficient permissions" }); + return; + } - async apply({ pluginData, actionConfig, actionName, interaction }) { - interaction.deferReply({ ephemeral: true }); - const targetMessage = interaction.channel - ? await interaction.channel.messages.fetch(interaction.targetId) - : await (pluginData.guild.channels.resolve(interaction.channelId) as TextChannel).messages.fetch( - interaction.targetId, - ); - - const amount = actionConfig.amount ?? 10; - const targetUserOnly = actionConfig.targetUserOnly ?? false; - const deletePins = actionConfig["delete-pins"] ?? false; - - const user = targetUserOnly ? targetMessage.author.id : undefined; - const targetMember = await pluginData.guild.members.fetch(targetMessage.author.id); - const executingMember = await pluginData.guild.members.fetch(interaction.user.id); - const utility = pluginData.getPlugin(UtilityPlugin); - - if (targetUserOnly && !canActOn(pluginData, executingMember, targetMember)) { - interaction.followUp({ ephemeral: true, content: "Cannot clean users messages: insufficient permissions" }); - return; - } - - try { - interaction.followUp(`Cleaning... Amount: ${amount}, User Only: ${targetUserOnly}, Pins: ${deletePins}`); - utility.clean( - { count: amount, user, channel: targetMessage.channel.id, "delete-pins": deletePins }, - targetMessage, + 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 (e) { - 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).log(LogType.BOT_ALERT, { - body: `Failed to clean in <#${interaction.channelId}> in ContextMenu action \`${actionName}\``, - }); - } else { - throw e; - } + const targetUserOnly = false; + const deletePins = false; + const user = undefined; + + try { + interaction.followUp(`Cleaning... Amount: ${amount}, User Only: ${targetUserOnly}, Pins: ${deletePins}`); + const utility = pluginData.getPlugin(UtilityPlugin); + utility.clean({ count: amount, user, channel: targetMessage.channel.id, "delete-pins": deletePins }, targetMessage); + } catch (e) { + 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).log(LogType.BOT_ALERT, { + 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 00a9c131..af9b71ed 100644 --- a/backend/src/plugins/ContextMenus/actions/mute.ts +++ b/backend/src/plugins/ContextMenus/actions/mute.ts @@ -1,83 +1,60 @@ import humanizeDuration from "humanize-duration"; -import * as t from "io-ts"; +import { GuildPluginData } from "knub"; import { canActOn } from "src/pluginUtils"; import { LogType } from "../../../data/LogType"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { convertDelayStringToMS, tDelayString, tNullable } from "../../../utils"; +import { convertDelayStringToMS } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; -import { contextMenuAction } from "../helpers"; -import { resolveActionContactMethods } from "../utils/resolveActionContactMethods"; +import { ContextMenuPluginType } from "../types"; -export const MuteAction = contextMenuAction({ - configType: t.type({ - reason: tNullable(t.string), - duration: tNullable(tDelayString), - notify: tNullable(t.string), - notifyChannel: tNullable(t.string), - remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), - restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), - postInCaseLog: tNullable(t.boolean), - hide_case: tNullable(t.boolean), - }), +export async function muteAction(pluginData: GuildPluginData, duration, interaction) { + interaction.deferReply({ ephemeral: true }); + const executingMember = await pluginData.guild.members.fetch(interaction.user.id); + const userCfg = await pluginData.config.getMatchingConfig({ + channelId: interaction.channelId, + member: executingMember, + }); - defaultConfig: { - notify: null, // Use defaults from ModActions - hide_case: false, - }, + // TODO: Add perm check for can_mute + if (!userCfg.can_use) { + await interaction.followUp({ content: "Cannot mute: insufficient permissions" }); + return; + } - async apply({ pluginData, actionConfig, actionName, interaction }) { - const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined; - const reason = actionConfig.reason || "Context Menu Action"; - const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; - const rolesToRemove = actionConfig.remove_roles_on_mute; - const rolesToRestore = actionConfig.restore_roles_on_mute; + const durationMs = duration ? convertDelayStringToMS(duration)! : undefined; + const mutes = pluginData.getPlugin(MutesPlugin); + const userId = interaction.targetId; + const targetMember = await pluginData.guild.members.fetch(interaction.targetId); - const caseArgs: Partial = { - modId: pluginData.client.user!.id, - automatic: true, - postInCaseLogOverride: actionConfig.postInCaseLog ?? undefined, - hide: Boolean(actionConfig.hide_case), - }; + if (!canActOn(pluginData, executingMember, targetMember)) { + interaction.followUp({ ephemeral: true, content: "Cannot mute: insufficient permissions" }); + return; + } - interaction.deferReply({ ephemeral: true }); - const mutes = pluginData.getPlugin(MutesPlugin); - const userId = interaction.targetId; - const targetMember = await pluginData.guild.members.fetch(interaction.targetId); - const executingMember = await pluginData.guild.members.fetch(interaction.user.id); + const caseArgs: Partial = { + modId: executingMember.id, + }; - if (!canActOn(pluginData, executingMember, targetMember)) { - interaction.followUp({ ephemeral: true, content: "Cannot mute: insufficient permissions" }); - return; + try { + const result = await mutes.muteUser(userId, durationMs, "Context Menu Action", { caseArgs }); + + const muteMessage = `Muted **${result.case.user_name}** ${ + durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely" + } (Case #${result.case.case_number}) (user notified via ${result.notifyResult.method ?? + "dm"})\nPlease update the new case with the \`update\` command`; + + interaction.followUp({ ephemeral: true, content: muteMessage }); + } catch (e) { + 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).log(LogType.BOT_ALERT, { + body: `Failed to mute <@!${userId}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`, + }); + } else { + throw e; } - - try { - const result = await mutes.muteUser( - userId, - duration, - reason, - { contactMethods, caseArgs, isAutomodAction: true }, - rolesToRemove, - rolesToRestore, - ); - - const muteMessage = `Muted **${result.case.user_name}** ${ - duration ? `for ${humanizeDuration(duration)}` : "indefinitely" - } (Case #${result.case.case_number}) (user notified via ${result.notifyResult.method ?? - "dm"})\nPlease update the new case with the \`update\` command`; - - interaction.followUp({ ephemeral: true, content: muteMessage }); - } catch (e) { - 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).log(LogType.BOT_ALERT, { - body: `Failed to mute <@!${userId}> in ContextMenu action \`${actionName}\` because a mute role has not been specified in server config`, - }); - } else { - throw e; - } - } - }, -}); + } +} diff --git a/backend/src/plugins/ContextMenus/actions/userInfo.ts b/backend/src/plugins/ContextMenus/actions/userInfo.ts new file mode 100644 index 00000000..85bfae44 --- /dev/null +++ b/backend/src/plugins/ContextMenus/actions/userInfo.ts @@ -0,0 +1,21 @@ +import { GuildPluginData } from "knub"; +import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin"; +import { ContextMenuPluginType } from "../types"; + +export async function userInfoAction(pluginData: GuildPluginData, interaction) { + interaction.deferReply({ ephemeral: true }); + const executingMember = await pluginData.guild.members.fetch(interaction.user.id); + const userCfg = await pluginData.config.getMatchingConfig({ + channelId: interaction.channelId, + member: executingMember, + }); + + // TODO: Add can_userinfo perm check + if (userCfg.can_use) { + const util = pluginData.getPlugin(UtilityPlugin); + const embed = await util.userInfo(interaction.targetId, interaction.user.id); + await interaction.followUp({ embeds: [embed] }); + } else { + await interaction.followUp({ content: "Cannot info: insufficient permissions" }); + } +} diff --git a/backend/src/plugins/ContextMenus/helpers.ts b/backend/src/plugins/ContextMenus/helpers.ts deleted file mode 100644 index d6e4ac6c..00000000 --- a/backend/src/plugins/ContextMenus/helpers.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ContextMenuInteraction } from "discord.js"; -import * as t from "io-ts"; -import { GuildPluginData } from "knub"; -import { Awaitable } from "knub/dist/utils"; -import { ContextMenuPluginType } from "./types"; - -type ContextActionApplyFn = (meta: { - actionName: string; - pluginData: GuildPluginData; - actionConfig: TConfigType; - interaction: ContextMenuInteraction; -}) => Awaitable; - -export interface ContextActionBlueprint { - configType: TConfigType; - defaultConfig: Partial>; - - apply: ContextActionApplyFn>; -} - -export function contextMenuAction( - blueprint: ContextActionBlueprint, -): ContextActionBlueprint { - return blueprint; -} diff --git a/backend/src/plugins/ContextMenus/types.ts b/backend/src/plugins/ContextMenus/types.ts index 9b34cb6a..e73d686d 100644 --- a/backend/src/plugins/ContextMenus/types.ts +++ b/backend/src/plugins/ContextMenus/types.ts @@ -1,29 +1,17 @@ import * as t from "io-ts"; -import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub"; +import { BasePluginType, typedGuildEventListener } from "knub"; import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks"; -import { tNullable } from "../../utils"; -import { AvailableActions } from "./actions/availableActions"; - -export enum ContextMenuTypes { - USER = 2, - MESSAGE = 3, -} - -export const ContextMenuTypeNameToNumber: Record = { - USER: 2, - MESSAGE: 3, -}; - -const ContextActionOpts = t.type({ - enabled: tNullable(t.boolean), - label: t.string, - type: t.keyof(ContextMenuTypes), - action: t.partial(AvailableActions.props), -}); -export type TContextActionOpts = t.TypeOf; export const ConfigSchema = t.type({ - context_actions: t.record(t.string, ContextActionOpts), + can_use: t.boolean, + + user_muteindef: t.boolean, + user_mute1d: t.boolean, + user_mute1h: t.boolean, + user_info: t.boolean, + message_clean10: t.boolean, + message_clean25: t.boolean, + message_clean50: t.boolean, }); export type TConfigSchema = t.TypeOf; @@ -34,5 +22,4 @@ export interface ContextMenuPluginType extends BasePluginType { }; } -export const contextMenuCmd = typedGuildCommand(); export const contextMenuEvt = typedGuildEventListener(); diff --git a/backend/src/plugins/ContextMenus/utils/contextRouter.ts b/backend/src/plugins/ContextMenus/utils/contextRouter.ts index 5004807f..1bda0ff8 100644 --- a/backend/src/plugins/ContextMenus/utils/contextRouter.ts +++ b/backend/src/plugins/ContextMenus/utils/contextRouter.ts @@ -1,7 +1,7 @@ import { ContextMenuInteraction } from "discord.js"; import { GuildPluginData } from "knub"; -import { availableActions } from "../actions/availableActions"; import { ContextMenuPluginType } from "../types"; +import { hardcodedActions } from "./hardcodedContextOptions"; export async function routeContextAction( pluginData: GuildPluginData, @@ -9,20 +9,5 @@ export async function routeContextAction( ) { const contextLink = await pluginData.state.contextMenuLinks.get(interaction.commandId); if (!contextLink) return; - const contextActions = Object.entries(pluginData.config.get().context_actions); - - const configLink = contextActions.find(x => x[0] === contextLink.action_name); - if (!configLink) return; - - for (const [actionName, actionConfig] of Object.entries(configLink[1].action)) { - if (actionConfig == null) return; - const action = availableActions[actionName]; - action.apply({ - actionName, - pluginData, - actionConfig, - interaction, - }); - 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 index 93449819..074b4440 100644 --- a/backend/src/plugins/ContextMenus/utils/loadAllCommands.ts +++ b/backend/src/plugins/ContextMenus/utils/loadAllCommands.ts @@ -1,22 +1,25 @@ import { ApplicationCommandData } from "discord.js"; -import { LogType } from "src/data/LogType"; -import { LogsPlugin } from "src/plugins/Logs/LogsPlugin"; +import { LogType } from "../../../data/LogType"; +import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin"; import { GuildPluginData } from "knub"; -import { ContextMenuPluginType, ContextMenuTypeNameToNumber } from "../types"; +import { ContextMenuPluginType } from "../types"; +import { hardcodedContext } from "./hardcodedContextOptions"; export async function loadAllCommands(pluginData: GuildPluginData) { const comms = await pluginData.client.application!.commands; - const actions = pluginData.config.get().context_actions; + const cfg = pluginData.config.get(); const newCommands: ApplicationCommandData[] = []; const addedNames: string[] = []; - for (const [name, configAction] of Object.entries(actions)) { - if (!configAction.enabled) continue; + for (const [name, label] of Object.entries(hardcodedContext)) { + if (!cfg[name]) continue; + const type = name.startsWith("user") ? 2 : 3; const data: ApplicationCommandData = { - type: ContextMenuTypeNameToNumber[configAction.type], - name: configAction.label, + type, + name: label, }; + addedNames.push(name); newCommands.push(data); } diff --git a/backend/src/plugins/ContextMenus/utils/resolveActionContactMethods.ts b/backend/src/plugins/ContextMenus/utils/resolveActionContactMethods.ts deleted file mode 100644 index bec64765..00000000 --- a/backend/src/plugins/ContextMenus/utils/resolveActionContactMethods.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Snowflake, TextChannel } from "discord.js"; -import { GuildPluginData } from "knub"; -import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { disableUserNotificationStrings, UserNotificationMethod } from "../../../utils"; -import { ContextMenuPluginType } from "../types"; - -export function resolveActionContactMethods( - pluginData: GuildPluginData, - actionConfig: { - notify?: string | null; - notifyChannel?: string | null; - }, -): UserNotificationMethod[] { - if (actionConfig.notify === "dm") { - return [{ type: "dm" }]; - } else if (actionConfig.notify === "channel") { - if (!actionConfig.notifyChannel) { - throw new RecoverablePluginError(ERRORS.NO_USER_NOTIFICATION_CHANNEL); - } - - const channel = pluginData.guild.channels.cache.get(actionConfig.notifyChannel as Snowflake); - if (!(channel instanceof TextChannel)) { - throw new RecoverablePluginError(ERRORS.INVALID_USER_NOTIFICATION_CHANNEL); - } - - return [{ type: "channel", channel }]; - } else if (actionConfig.notify && disableUserNotificationStrings.includes(actionConfig.notify)) { - return []; - } - - return []; -} diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index a2e9a6e7..9a624ed8 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -1,3 +1,4 @@ +import { MessageEmbedOptions } from "discord.js"; import { PluginOptions } from "knub"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; @@ -36,6 +37,7 @@ import { UserInfoCmd } from "./commands/UserInfoCmd"; import { VcdisconnectCmd } from "./commands/VcdisconnectCmd"; import { VcmoveAllCmd, VcmoveCmd } from "./commands/VcmoveCmd"; import { AutoJoinThreadEvt, AutoJoinThreadSyncEvt } from "./events/AutoJoinThreadEvt"; +import { getUserInfoEmbed } from "./functions/getUserInfoEmbed"; import { activeReloads } from "./guildReloads"; import { refreshMembersIfNeeded } from "./refreshMembers"; import { ConfigSchema, UtilityPluginType } from "./types"; @@ -162,6 +164,12 @@ export const UtilityPlugin = zeppelinGuildPlugin()({ cleanCmd(pluginData, args, msg); }; }, + + userInfo(pluginData) { + return (userId: string, requestMemberId?: string) => { + return getUserInfoEmbed(pluginData, userId, false, requestMemberId); + }; + }, }, beforeLoad(pluginData) {