Skip to content

Context menu commands

Add a right-click command on a user or a message, and read what someone ran it on. Covers the base class each kind has, reaching the member, serving several commands from one handler, and the separate names each kind keeps.

Discord shows a context menu command when someone right-clicks. It takes a name and a kind, and the click itself is its only input.

src/commands/ViewProfile.ts
import {
    BuilderComponent,
    RegisterCommand
} from '@seedcord/gateway';
import { ApplicationCommandType } from 'discord.js';

@RegisterCommand('global')
export class ViewProfile extends BuilderComponent<'context_menu'> {
    constructor() {
        super('context_menu');

        this.instance
            .setName('View Profile')
            .setType(ApplicationCommandType.User);
    }
}

That name is the route you write on the handler, including its spaces and capitals.

src/handlers/ViewProfile.tshover for typestap for types, arrow keys walk the tokens
@UserContextMenuRoute<"View Profile">(...names: "View Profile"[]): <TCtor>(constructor: AssertContextMenuRoute<ApplicationCommandType.User, "View Profile", TCtor>) => voidUserContextMenuRoute('View Profile')
export class class ViewProfileViewProfile extends 
class UserContextMenuHandler<
    Names extends NamesFor<ApplicationCommandType.User>,
    Cache extends CacheType = MenuCacheFor<
        ApplicationCommandType.User,
        Names
    >
>
UserContextMenuHandler
<'View Profile'> {
public async ViewProfile.execute(): Promise<void>execute(): interface Promise<T>Promise<void> { const const user: Useruser = this.UserContextMenuHandler<"View Profile", "cached">.target: Usertarget; await this.RepliableHandler<UserContextMenuCommandInteraction<"cached">, Core, SentMessage, BufferResolvable | ... 4 more ... | AttachmentPayload, ReplySender>.reply(response: string | ReplyResponse<BufferResolvable | Stream | JSONEncodable<APIAttachment> | Attachment | AttachmentBuilder | AttachmentPayload>, opts?: SendOpts): Promise<SentMessage>reply(`Profile for ${const user: Useruser.User.tag: stringtag}.`); } }

Each kind has its own base class

ApplicationCommandType.User puts the command on a right-clicked person. ApplicationCommandType.Message puts it on a right-clicked message. Pick the base and the decorator that match the kind you set.

the command setsthe handler extendsthe decorator
ApplicationCommandType.UserUserContextMenuHandler@UserContextMenuRoute
ApplicationCommandType.MessageMessageContextMenuHandler@MessageContextMenuRoute

Gateway and http differ

The clicked thing arrives as a discord.js object on gateway and as a discord-api-types payload on http.

gatewayhttp
user this.targetUserAPIUser
message this.targetMessageAPIMessage
this.targetMemberGuildMember | nullAPIInteractionDataResolvedGuildMember | null

Both transports give this.target an id. A user target carries username too, and a message target reaches its author through author.

this.targetMember differs further. Gateway gives it id and displayName. Http holds the guild fields alone there, since Discord sends that person's user object once under resolved.users.

A message command reads the message it was run on.

src/handlers/ReportMessage.tshover for typestap for types, arrow keys walk the tokens
@MessageContextMenuRoute<"Report Message">(...names: "Report Message"[]): <TCtor>(constructor: AssertContextMenuRoute<ApplicationCommandType.Message, "Report Message", TCtor>) => voidMessageContextMenuRoute('Report Message')
export class class ReportMessageReportMessage extends 
class MessageContextMenuHandler<
    Names extends NamesFor<ApplicationCommandType.Message>,
    Cache extends CacheType = MenuCacheFor<
        ApplicationCommandType.Message,
        Names
    >
>
MessageContextMenuHandler
<'Report Message'> {
public async ReportMessage.execute(): Promise<void>execute(): interface Promise<T>Promise<void> { const const message: Message<true>message = this.MessageContextMenuHandler<"Report Message", "cached">.target: Message<true>target; await this.RepliableHandler<MessageContextMenuCommandInteraction<"cached">, Core, SentMessage, BufferResolvable | ... 4 more ... | AttachmentPayload, ReplySender>.reply(response: string | ReplyResponse<BufferResolvable | Stream | JSONEncodable<APIAttachment> | Attachment | AttachmentBuilder | AttachmentPayload>, opts?: SendOpts): Promise<SentMessage>reply( `Reported ${const message: Message<true>message.Message<true>.id: stringid} from <@${const message: Message<true>message.Message<true>.author: Userauthor.User.id: stringid}>.` ); } }

Reaching the member

A user command also carries the clicked person's server member, which is null when the command runs outside a server.

const member = this.targetMember;
const member: GuildMember | null

Several commands in one handler

You put both commands on one class. Its decorator takes every name and its generic repeats them. Each arm then receives the clicked user and their server member, narrowed to that one command.

src/handlers/Moderation.ts
@UserContextMenuRoute('View Profile', 'Warn')
export class Moderation extends UserContextMenuHandler<
    'View Profile' | 'Warn'
> {
    public async execute(): Promise<void> {
        await this.match({
            'View Profile': (user) =>
                this.reply(`Profile for ${user.tag}.`),
            Warn: (user, member) =>
                this.reply(
                    `Warned ${member?.displayName ?? user.tag}.`
                )
        });
    }
}

Leaving Warn out of the arms is a compile error, the same as it is on a slash handler.

this.commandName gives you the name that fired, typed to the union, for when you want it without branching.

Message commands work the same way, and each of your arms receives the clicked message.

Each kind keeps its own names

Discord lets a user command and a message command share a name, so the two registries stay separate. A name in one is invisible to the other.

class Wrong extends MessageContextMenuHandler<'View Profile'> {
Type '"View Profile"' does not satisfy the constraint '"Report Message"'.
public async execute(): Promise<void> {} }