Context menus
Add a command to the Apps menu of a user or a message, and handle it like a slash command.
You'll learn
- Describe a user or message command with a builder
- Read the user or message the menu was opened on
- Test a context menu command with its target
Before this
A context menu command appears when a member right-clicks a user or a message and opens Apps: "Report user", "Bookmark", "Translate". It takes no options. What it acts on is the user or message the menu was opened on, which the interaction carries as its target.
When to use it
Use a context menu when a command's subject is a particular user or message, and picking it by right-click is more natural than pasting an id: reporting a message, bookmarking it, looking up a member's profile.
When the member has to type something, such as a reason or an amount, a slash command with
options fits better, or a context menu that opens a form with respond(interaction).modal(...).
Example
A user command's builder, with its type set to User:
// A context menu's name is what the menu shows, capitals and spaces included
@CommandBuilder(CommandType.CONTEXT_MENU)
export class ReportUserBuilder {
build(commandName: string) {
return new ContextMenuCommandBuilder().setName(commandName).setType(ApplicationCommandType.User)
}
}// Right-click a member, then Apps › Report user
@Command('Report user', ReportUserBuilder)
async report(interaction: UserContextMenuCommandInteraction) {
await respond(interaction).send({
content: `Thanks, ${interaction.targetUser.username} was reported to the staff.`,
flags: MessageFlags.Ephemeral,
})
}"Report user" appears in a member's Apps menu. The handler reads the member it was opened on from
interaction.targetUser, and thanks the reporter privately.
How it works
A context menu command is registered and routed like a slash command:
- The builder returns a
ContextMenuCommandBuilder, with its typeUserorMessage, under@CommandBuilder(CommandType.CONTEXT_MENU). Registering commands applies as it does to slash commands. - The name is what the menu shows. Unlike a slash command's, it may hold capitals and spaces, and
@Commandtakes it as it is:@Command('Report user', ReportUserBuilder). A user command and a message command may share a name, since Discord keeps them apart by type, and each reaches its own handler. - The handler receives a
UserContextMenuCommandInteractionor aMessageContextMenuCommandInteraction, and no options. It runs through the full pipeline and answers withrespond().
Message commands
A message command's builder sets the type Message, and its handler reads interaction.targetMessage:
// Right-click a message, then Apps › Bookmark
@Command('Bookmark', BookmarkBuilder)
async bookmark(interaction: MessageContextMenuCommandInteraction) {
await interaction.user.send({ content: `Bookmarked: ${interaction.targetMessage.url}` })
await respond(interaction).send({ content: 'Sent to your DMs.', flags: MessageFlags.Ephemeral })
}The handler's type
A handler declares the kind of interaction its builder registers: UserContextMenuCommandInteraction for a builder that
sets ApplicationCommandType.User, and MessageContextMenuCommandInteraction for Message, as the examples do.
MeoCord reads the kind from the builder's setType(), so a handler that declares the other kind doesn't compile,
however its interaction is imported. The error is on its @Command: "Unable to resolve signature of method decorator
when called as an expression".
A builder whose kind the compiler can't tell, one whose build() declares its return type as
ContextMenuCommandBuilder or that picks the kind at runtime, lets its handler declare either. MeoCord then checks the
kind as the controller loads: a handler of the other kind stops the bot, naming the handler and the builder. That check
reads the decorator metadata the compiler emits, so it needs the interaction class imported as a value, as the generated
controller does; import { type … } erases it.
A handler that serves both kinds takes their union, and narrows it with isUserContextMenuCommand() or
isMessageContextMenuCommand(). meocord g co context-menu Report generates a user command with its handler typed to
match, and --message a message one.
Testing
Give the mock its target in the overrides, since discord.js makes targetUser and targetMessage read-only. For a
user command, a targetId is enough: targetUser is the client's cached user with that id, or one made, and in a
server targetMember is their member:
describe('ReportContextMenuController', () => {
const module = MeoCordTestingModule.create({ controllers: [ReportContextMenuController] }).compile()
it('reports the member the menu was opened on', async () => {
const target = createMockUser()
Object.assign(target, { username: 'mika' })
const interaction = createMockInteraction(UserContextMenuCommandInteraction, {
commandName: 'Report user',
targetUser: target,
})
await module.invoke(ReportContextMenuController, 'report', interaction)
expect(getResponse(interaction).calls[0].payload).toMatchObject({
content: 'Thanks, mika was reported to the staff.',
})
})
it('DMs a link to the message the menu was opened on', async () => {
const targetMessage = createMockMessage({ id: '1300000000000000000' })
Object.assign(targetMessage, { url: 'https://discord.com/channels/1/2/1300000000000000000' })
const interaction = createMockInteraction(MessageContextMenuCommandInteraction, {
commandName: 'Bookmark',
targetMessage,
})
await module.invoke(ReportContextMenuController, 'bookmark', interaction)
expect(interaction.user.send).toHaveBeenCalledWith({ content: `Bookmarked: ${targetMessage.url}` })
})
})Gotchas
- A menu the member can't find. Context menu commands appear under Apps when right-clicking, not in the
/list. Say so in your bot's help. - A handler typed for the other kind. A handler declaring
MessageContextMenuCommandInteractionon a builder that setsUserdoesn't compile. Match the handler to the builder'ssetType(). - A name mismatch. The name in
@Commandis the one the builder receives. Build withsetName(commandName), so the menu and the handler can't disagree. - Replying in public by accident. A report or a bookmark is usually for the member alone; send it with
MessageFlags.Ephemeral.
Next steps
- Where the interaction happened: servers, DMs and user installs.
- Components: the form a context menu can open.
- Answering with respond(): private answers and follow-ups.