What you can build
Every kind of handler MeoCord runs, from slash commands to gateway events, each with a small working example.
Before this
A MeoCord bot is controllers, and a controller's methods are its handlers. Each kind of handler answers one thing a member does in Discord: runs a command, clicks a button, sends a message, reacts, or joins a server. Here is each kind, with a small example that compiles and runs, and the page that teaches it.
Slash commands
A member types /echo and picks its options. The builder describes the command Discord shows, and the handler
answers it:
// Describes the command to Discord: its name comes from @Command, the rest from here
@CommandBuilder(CommandType.SLASH)
export class EchoCommandBuilder {
build(commandName: string) {
return new SlashCommandBuilder()
.setName(commandName)
.setDescription('Repeats what you say, to you alone')
.addStringOption(option => option.setName('text').setDescription('What to repeat').setRequired(true))
}
}@Controller()
export class EchoSlashController {
// /echo text:hello gives { text: 'hello' }
@Command('echo', EchoCommandBuilder)
async echo(interaction: ChatInputCommandInteraction, { text }: { text: string }) {
await respond(interaction).send({ content: text, flags: MessageFlags.Ephemeral })
}
}Slash commands covers options, registering and entry point commands.
Subcommands
One command with groups and subcommands, such as /settings notify email, each routed to a handler of its own:
@Controller()
export class SettingsSlashController {
// The builder is declared once, on the command itself
@Command('settings', SettingsCommandBuilder)
async settings(interaction: ChatInputCommandInteraction) {
await respond(interaction).send({ content: 'Pick a subcommand.' })
}
// The full path, as Discord displays it; plain CommandType.SLASH and no builder
@Command('settings notify email', CommandType.SLASH)
async notifyEmail(interaction: ChatInputCommandInteraction, { enabled }: { enabled: boolean }) {
await respond(interaction).send({ content: `Email notifications ${enabled ? 'on' : 'off'}` })
}
}Subcommands covers groups and the builder.
Buttons
A button's custom ID carries what the handler needs, read back as typed params:
// customId `profile/123/800000001` gives ownerId '123' and uid '800000001'
@Command('profile/{ownerId}/{uid}', CommandType.BUTTON)
async showProfile(interaction: ButtonInteraction, { ownerId, uid }: { ownerId: string; uid: string }) {
await respond(interaction).send({ content: `Profile ${uid}, opened by <@${ownerId}>` })
}Buttons, selects and modals covers patterns and routing.
Select menus
A member picks values from a menu, and the handler gets them:
@Controller()
export class PollSelectMenuController {
// poll/{pollId} captures the poll; values holds the options the member chose
@Command('poll/{pollId}', CommandType.SELECT_MENU)
async vote(interaction: StringSelectMenuInteraction, { pollId, values }: { pollId: string; values: string[] }) {
await respond(interaction).send({
content: `Poll ${pollId}: you picked ${values.join(', ')}.`,
flags: MessageFlags.Ephemeral,
})
}
}Select menus covers the user, role, channel and mentionable menus too.
Modals
A form a member fills in and submits, routed by its custom ID:
// The second argument holds the captured `ticketId` and the submitted `body` field
@Command('feedback/{ticketId}', CommandType.MODAL_SUBMIT)
async submit(interaction: ModalSubmitInteraction, { ticketId, body }: { ticketId: string; body: string }) {
await respond(interaction).send({ content: `Ticket ${ticketId}: ${body}`, flags: MessageFlags.Ephemeral })
}Modals covers opening one and reading its fields.
Context menus
A member right-clicks a user or a message and picks the bot's command:
// 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,
})
}Context menus covers message context menus and the builder.
Autocomplete
The bot suggests values for an option as the member types it:
@Controller()
export class SearchSlashController {
constructor(private readonly catalog: CatalogService) {}
@Command('search', SearchCommandBuilder)
async search(interaction: ChatInputCommandInteraction, { query }: { query: string }) {
await respond(interaction).send({ content: `Results for ${query}` })
}
@Autocomplete('search', 'query')
async completeQuery(interaction: AutocompleteInteraction) {
const { value } = interaction.options.getFocused(true)
// Discord shows at most 25 choices
const matches = this.catalog.find(value).slice(0, 25)
await interaction.respond(matches.map(name => ({ name, value: name })))
}
}Autocomplete covers where the suggestions come from and their limits.
Message commands
A member sends !roll 20 for initiative, and the pattern and its schema hand the handler its values, typed:
// !roll 20 for initiative gives { sides: 20, note: 'for initiative' }
@MessageHandler('roll {sides} {note...?}')
@Validate(z.object({ sides: z.coerce.number().int().min(2).max(100), note: z.string().optional() }))
async roll(message: Message, { sides, note }: { sides: number; note?: string }) {
const result = 1 + Math.floor(Math.random() * sides)
await message.reply(note ? `${result} (${note})` : String(result))
}Message commands covers prefixes, flags, lists and aliases.
Reactions
A member reacts to a message with an emoji, and the bot acts on it:
import { type MessageReaction, type PartialMessageReaction } from 'discord.js'
import { Controller, ReactionHandler } from 'meocord/decorator'
import { ReactionHandlerAction } from 'meocord/enum'
import { type ReactionEvent } from 'meocord/interface'
@Controller()
export class StarReactionController {
@ReactionHandler('⭐')
async star(reaction: MessageReaction | PartialMessageReaction, { user, action }: ReactionEvent) {
if (action !== ReactionHandlerAction.ADD || user.bot) return
await reaction.message.reply(`${user.username} starred this.`)
}
}Reactions and other messages covers removals and partial messages.
Gateway events
Anything else Discord tells the bot, such as a member joining a server:
import { type Client, type GuildMember } from 'discord.js'
import { Controller, On, Once } from 'meocord/decorator'
@Controller()
export class WelcomeController {
// Every time a member joins; needs the GuildMembers intent
@On('guildMemberAdd')
async greet(member: GuildMember) {
await member.send(`Welcome to ${member.guild.name}!`)
}
// The first time the client is ready, and never again
@Once('clientReady')
async warmCache(client: Client<true>) {
await client.guilds.fetch()
}
}Gateway events covers every event and when a handler runs.
Next steps
- Your first command: build one of these from scratch, and run it.
- Example bots: whole bots that put these handlers together, with their tests.
- How a call runs: what happens between the member's click and your handler.
- Services: share state and work between handlers, injected where they're needed.