# 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 ```