# Commands
`ExGram` provides a `command` macro to declare bot commands with descriptions, visibility scopes, and language translations. When `setup_commands: true` is set on the bot, these declarations are automatically registered with the Telegram API on startup, making your commands appear in the autocomplete menu for users.
## Basic usage
The simplest way to declare a command is with just a name and a description:
```elixir
defmodule MyBot do
use ExGram.Bot, name: :my_bot, setup_commands: true
command(:start, description: "Start the bot")
def handle({:command, :start, _msg}, context) do
answer(context, "Welcome!")
end
end
```
Commands without a `description` are still valid - they will be handled by your bot but won't be registered in the Telegram command menu. This is useful for "hidden" commands or not useful commands in day-to-day:
```elixir
command(:start) # no description - works but won't show in menu
command(:debug) # no description - works but won't show in menu
def handle({:command, :start, _msg}, context) do
answer(context, "Hey!")
end
def handle({:command, :debug, _msg}, context) do
answer(context, "Debug mode")
end
```
## Options reference
- `:description` (string) - text shown in Telegram's command menu. Required if any other of this options are set.
- `:scopes` (list) - scopes where the command is visible. See [Scopes](#scopes).
- `:lang` (keyword list) - language-specific overrides, IETF language code atom. See [Language translations](#language-translations).
- `:name` (atom) - atom used for dispatch pattern matching. Defaults to the command name as an atom. Useful when you want a different name in the handler.
```elixir
# Override the handler atom - dispatches as {:command, :begin, msg}
command(:start, name: :begin, description: "Start the bot")
def handle({:command, :begin, _msg}, context) do
answer(context, "Welcome!")
end
```
## Scopes
Telegram's [BotCommandScope](https://core.telegram.org/bots/api#botcommandscope) controls where commands appear in the autocomplete menu. By declaring scopes you can show different command sets to different audiences - for example, showing admin commands only to group administrators.
### Simple scopes
These are plain atoms:
| Scope | Who sees it |
|----------------------------|-----------------------------------------------|
| `:default` | All users when no more specific scope applies |
| `:all_private_chats` | Users in any private (1-on-1) chat |
| `:all_group_chats` | Users in any group or supergroup chat |
| `:all_chat_administrators` | Administrators in any group chat |
```elixir
command(:help,
description: "Get help",
scopes: [:all_private_chats, :all_group_chats]
)
command(:ban,
description: "Ban a user",
scopes: [:all_chat_administrators]
)
```
### Parametric scopes
These are tuples that target specific chats or users:
- `{:chat, chat_ids: [100, 200]}` - visible in the listed chats. Expands to one API registration per chat.
- `{:chat_administrators, chat_ids: [100]}` - visible to administrators of the listed chats.
- `{:chat_member, chat_id: 1, user_ids: [10, 20]}` - visible to specific users in a specific chat.
```elixir
command(:notify,
description: "Send a notification",
scopes: [{:chat, chat_ids: [123_456, 789_012]}]
)
command(:secret,
description: "Secret command",
scopes: [{:chat_member, chat_id: 123_456, user_ids: [111, 222]}]
)
```
### Scope inheritance
The `scopes` option controls not just where a command appears, but how it interacts with the rest of your command list.
**Commands without `scopes`** (or with `scopes: nil`). All the other scopes will inherit this command, meaning they will appear everywhere other commands appear.
**Commands with `scopes: []`** (empty list) fall back to `:default`. It will only appear to users with the default scope.
**If no command defines any scope**, everything falls back to `:default`.
**Commands with explicit scopes** appear **only** in those scopes.
```elixir
# "help" has no scopes
command(:help, description: "Get help")
# "stats" is only in :all_private_chats
command(:stats,
description: "Your stats",
scopes: [:all_private_chats]
)
```
In this example, both `:stats` and `:help` appear in `:all_private_chats`. And on the `:default` scope (group chats for example) only the `:help` command would appear.
## Language translations
The `:lang` option lets you provide per-language overrides for the command name and description. Each key is an IETF language code atom (`:es`, `:pt`, `:it`, etc.) and the value is a keyword list with `:command` and/or `:description`.
### Translating descriptions
```elixir
command(:start,
description: "Start the bot",
scopes: [:default],
lang: [
es: [description: "Iniciar el bot"],
pt: [description: "Iniciar o bot"]
]
)
```
Spanish users see "Iniciar el bot", Portuguese users see "Iniciar o bot", everyone else sees "Start the bot".
### Translating command names
You can also change the command name itself for a language:
```elixir
command(:help,
description: "Get help",
scopes: [:default],
lang: [es: [command: "ayuda", description: "Obtener ayuda"]]
)
```
Spanish users see `/ayuda` in their menu. ExGram automatically registers `/ayuda` as a dispatch alias, so it routes to the same `:help` handler - no extra `handle` clause needed:
```elixir
def handle({:command, :help, _msg}, context) do
# Here the user will have `language_code` if you want to send translated messages
answer(context, "Here is some help!")
end
```
### Inheriting values
A lang entry does not need to override both fields:
- Omitting `:description` inherits the base description.
- Omitting `:command` keeps the base command name.
```elixir
command(:help,
description: "Get help",
lang: [es: [command: "ayuda"]] # description inherited from base
)
```
### Merge behavior
Untranslated commands are automatically merged into every language group. This ensures users of any language always see the full command list - translated commands appear in their translated form, untranslated commands fall back to their base form.
```elixir
command(:start,
description: "Start the bot",
lang: [es: [description: "Iniciar el bot"]]
)
command(:help, description: "Get help")
```
Spanish users see both `start` ("Iniciar el bot") and `help` ("Get help"). The untranslated `:help` is merged in automatically.
If a command renames itself in a translation (e.g. `help` -> `ayuda`), the original name (`help`) is excluded from that language group to avoid showing duplicate entries.
## Scopes and languages combined
Translations apply independently to each scope. If a command appears in multiple scopes, each (scope + language) combination gets its own Telegram API registration.
```elixir
command(:greet,
description: "Greet users",
scopes: [:all_private_chats, :all_group_chats],
lang: [es: [description: "Saludar usuarios"]]
)
```
This produces four registrations:
- `:all_private_chats` (no lang) - "Greet users"
- `:all_group_chats` (no lang) - "Greet users"
- `:all_private_chats` + `"es"` - "Saludar usuarios"
- `:all_group_chats` + `"es"` - "Saludar usuarios"
## Full example
```elixir
defmodule MyBot do
use ExGram.Bot, name: :my_bot, setup_commands: true
middleware(ExGram.Middleware.IgnoreUsername)
# Visible to all users in all contexts
command(:start,
description: "Start the bot",
lang: [
es: [description: "Iniciar el bot"],
pt: [description: "Iniciar o bot"]
]
)
# Only visible in private chats, with a translated name for Spanish
command(:help,
description: "Get help",
scopes: [:all_private_chats],
lang: [es: [command: "ayuda", description: "Obtener ayuda"]]
)
# Only visible to group administrators
command(:ban,
description: "Ban a user",
scopes: [:all_chat_administrators],
lang: [es: [description: "Prohibir usuario"]]
)
# Hidden command - no description, won't appear in the menu
command(:debug)
def handle({:command, :start, _msg}, context) do
answer(context, "Welcome! Use /help for a list of commands.")
end
# Handles both /help and /ayuda (Spanish alias)
def handle({:command, :help, _msg}, context) do
answer(context, "Here is some help!")
end
def handle({:command, :ban, %{text: target}}, context) do
answer(context, "Banned #{target}")
end
def handle({:command, :debug, _msg}, context) do
answer(context, "Debug info: ...")
end
def handle(_, _context), do: :ok
end
```