Skip to content

User, message and entry point commands

Besides slash commands, Discord has commands that run from the context menu of a user or a message, and the entry point command that opens an app's Activity.

User and message commands

import bostie
from bostie import Context, Message, ResolvedMember, User, WebhookMessage, fmt

bot = bostie.Bot()


@bot.user_command("Account age")
async def account_age(ctx: Context, user: User) -> None:
    created = fmt.timestamp(user.created_at, "R")
    await ctx.respond(f"{user.mention} joined Discord {created}.", ephemeral=True)


@bot.user_command("Joined server")
async def joined(ctx: Context, member: ResolvedMember) -> None:
    if member.joined_at is None:
        await ctx.respond(f"Discord sent no join date for {member.mention}.")
        return
    await ctx.respond(f"{member.mention} joined {fmt.timestamp(member.joined_at)}.")


@bot.message_command("Word count")
async def word_count(ctx: Context, message: Message | WebhookMessage) -> None:
    await ctx.respond(f"{len(message.content.split())} words.", ephemeral=True)

@bot.user_command(name) and @bot.message_command(name) register a function that takes the context and the target. The name is what the menu shows: 1 to 32 characters, and unlike slash command names it may hold uppercase letters and spaces. These commands have no description and no options.

The target's type hint says what the handler gets:

Command Hint The handler receives
user User the user
user ResolvedMember the member, with its user; only in guilds
user User \| ResolvedMember the member in a guild, the user elsewhere
message Message a message a user or bot sent
message WebhookMessage a message a webhook sent, interaction answers included
message Message \| WebhookMessage either

A target that does not resolve to the hinted type raises OptionConversionError, which goes to the error hooks. Hint a message target as Message | WebhookMessage unless the command only makes sense for one of them.

They take the settings of a slash command except description, description_localizations and the option settings: name_localizations, ephemeral, auto_defer, default_permissions, nsfw, guilds, contexts, integration_types, checks and extra. Discord allows 15 user and 15 message commands per scope, and the sync warns above that.

The entry point command

An app with Activities has one entry point command, which the App Launcher shows. @bot.entry_point(name) registers it with a function that takes only the context:

import bostie
from bostie import Context

bot = bostie.Bot()


@bot.entry_point("Launch", description="Open the game")
async def launch(ctx: Context) -> None:
    await ctx.launch_activity()

The handler answers like any command: ctx.launch_activity() opens the Activity and returns the id of its instance, and ctx.respond(...) answers with a message instead. Discord refuses the command from an app without Activities enabled in the Developer Portal.

The entry point is always global. With dev_guilds set, the sync leaves it out and warns. When Discord already has its default entry point under another name, the sync keeps that command's id, so Discord replaces it instead of keeping both. launch_activity() exists on component and modal contexts too, but it has to be the first answer.

entry_point takes description, name_localizations, description_localizations, ephemeral, auto_defer, default_permissions, nsfw, contexts, integration_types, checks and extra. Activities, monetization and the Social SDK covers the rest of the Activity side.