> ## Documentation Index
> Fetch the complete documentation index at: https://poly-bot.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Bot Commands: Routing, Arguments & BotFather Sync in PolyBot

> Define slash commands with typed argument extraction, optional parameters, and automatic BotFather menu synchronization using the [Command] attribute.

PolyBot maps incoming Telegram commands to handler methods declaratively. Decorate any `[MessageHandler]` method with `[Command]` to restrict it to messages whose first entity is a bot command. The source generator emits all entity-checking, argument-parsing, and `@botname`-suffix verification code so you never write routing boilerplate by hand.

Aliases can be declared positionally: `[Command("start", "begin")]` is shorthand for `[Command(Aliases = ["start", "begin"])]` — both forms are equivalent and can be combined.

## The `[Command]` attribute

```csharp theme={null}
[MessageHandler]
[Command(
    Aliases = ["start"],
    Description = "Start the bot",
    Scope = CommandScope.AllPrivateChats,
    LanguageCode = "en",
    IsHidden = false
)]
public static async Task<Result> StartHandler(Message msg, ITelegramBotClient bot, CancellationToken ct)
{
    await bot.SendMessage(msg.Chat.Id, "Welcome!", cancellationToken: ct);
    return Result.Handled();
}
```

### Properties at a glance

| Property | Type | Default | Purpose |
| - | - | - | - |
| `Aliases` | `string[]` | `[]` | Command names without the leading `/`, matched case-insensitively |
| `Prefix` | `char` | `'/'` | Controls the matching mode (see below) |
| `NoArgs` | `bool` | `false` | Throws `CommandArgsParseException` if the message carries any tokens after the command |
| `Description` | `string?` | `null` | BotFather menu description; missing values are a compile-time warning (`CUR021`) and the command is routed but not synced; values over 256 chars are a compile-time error (`CUR022`) |
| `LanguageCode` | `string?` | `null` | BotFather localization code (e.g. `"en"`, `"ru"`); `null` registers for all languages |
| `Scope` | `CommandScope` | `Default` | The BotFather audience for the command |
| `IsHidden` | `bool` | `false` | Routes normally but is excluded from BotFather sync and skips the missing-description warning |

### How command matching works

When `Prefix` is `'/'` (the default), PolyBot checks that the message's first entity is of type `BotCommand`. If the command text includes an `@botname` suffix — such as `/start@MyBot` — the suffix is verified against `PolyBotOptions.BotUsername`, which is populated automatically by `PolyBotClient.RunPollingAsync()` via `GetMe`. A mismatched suffix falls through to the next handler; other handlers (like an echo fallback) continue routing normally.

```csharp theme={null}
// /start@TestBot  → matched (username matches)
// /start@WrongBot → falls through to echo
[MessageHandler]
[Command(Aliases = ["start"])]
public static Task<Result> Start(Message msg) { ... }
```

For non-`'/'` prefixes, PolyBot matches by `string.StartsWith` (ordinal-ignore-case) against `Message.Text` directly — no entity required. A space (`' '`) or null character (`'\0'`) prefix matches text starting with the alias alone.

```csharp theme={null}
// Triggered by "!hi"
[MessageHandler]
[Command(Aliases = ["hi"], Prefix = '!')]
public static Task<Result> Hi(Message msg) { ... }

// Triggered by "time" (no prefix)
[MessageHandler]
[Command(Aliases = ["time"], Prefix = ' ')]
public static Task<Result> Time(Message msg) { ... }
```

### Guest messages

Guest updates arrive with the bot's mention first — `@PolyBot /start` — and Telegram marks that mention with a `Mention` entity at offset 0. On a `[GuestMessageHandler]`, `[Command]` uses that entity to find where the command begins and matches from there, so the example below handles only `/start`:

```csharp theme={null}
// "@PolyBot /start"     → matched
// "@PolyBot /start arg" → matched, [Arg] binds from "arg"
// "/start" (no mention) → NOT matched; route it with a regular [MessageHandler]
[GuestMessageHandler]
[Command(Aliases = ["start"])]
public static Task<Result> GuestStart(Message msg) { ... }
```

The mention itself is not verified by name — Telegram only delivers guest messages for the bot that was mentioned, and matching is entity-driven: text that merely *looks* like `@BotName /cmd` without a `Mention` entity does not match. The `@botsuffix` check on the command entity itself (`/start@MyBot`) still applies. Non-`'/'` prefixes work the same way: the remainder after the mention entity is matched as if it were the whole text. Imperative code can use the same rule via `CommandHelper.MatchGuestCommand`.

> [!TIP]
>
> Replies to guest messages never go through `SendMessage` — answer the guest query with `AnswerGuestQuery` and the message's `GuestQueryId`. The generator reports a `CUR046` warning when a `[GuestMessageHandler]` calls `SendMessage`, and a `CUR047` warning when it answers the same guest query more than once. See the [diagnostics reference](https://poly-bot.mintlify.site/reference/diagnostics.md).

### `CommandScope` values

The `Scope` property maps to Telegram's `BotCommandScope` and controls which chats see the command in the BotFather menu.

| Value | BotFather audience |
| - | - |
| `Default` | All chats (the default) |
| `AllPrivateChats` | Private chats only |
| `AllGroupChats` | Group and supergroup chats |
| `AllChatAdministrators` | Chat administrators across all groups |

## Argument extraction

Arguments are the whitespace-separated tokens that follow the command word in the message text. Three attributes let you bind handler parameters to those tokens instead of resolving them from the DI container.

> [!WARNING]
>
> `[Arg]`, `[Rest]`, and `[Parse]` are only meaningful on `[Command]`-restricted handlers. Placing them on a handler without `[Command]` produces warning `CUR003` and the parameters are treated as normal DI dependencies.

### `[Arg]` — single token

Binds one whitespace-separated token by position. The token is converted using `TryParse` (invariant culture for numeric types). Set `IsOptional = true` to yield the parameter's default value instead of a parse error when the token is absent.

```csharp theme={null}
[MessageHandler]
[Command(Aliases = ["greet"])]
public static async Task<Result> Greet(
    Message msg,
    ITelegramBotClient bot,
    [Arg] string name,
    [Arg(IsOptional = true)] int? times,
    CancellationToken ct)
{
    int repeat = times ?? 1;
    string greeting = string.Join(", ", Enumerable.Repeat($"Hello, {name}!", repeat));
    await bot.SendMessage(msg.Chat.Id, greeting, cancellationToken: ct);
    return Result.Handled();
}
// "/greet Alice"    → "Hello, Alice!"
// "/greet Alice 3"  → "Hello, Alice!, Hello, Alice!, Hello, Alice!"
```

> [!NOTE]
>
> `[Arg]` parameter types must be the argument type, `Nullable<T>`, or `object`. Any other type produces warning `CUR005` and falls back to DI injection.

### `[Rest]` — remainder string

Binds everything that follows the positionally consumed `[Arg]` tokens as a single trimmed `string`. At most one `[Rest]` parameter is allowed per handler; more produce warning `CUR006`.

```csharp theme={null}
[MessageHandler]
[Command(Aliases = ["search"])]
public static async Task<Result> Search(
    Message msg,
    ITelegramBotClient bot,
    [Rest] string query,
    CancellationToken ct)
{
    await bot.SendMessage(msg.Chat.Id, $"Searching for: {query}", cancellationToken: ct);
    return Result.Handled();
}
// "/search the quick brown fox" → "Searching for: the quick brown fox"
```

### `[Parse]` — regex match

Binds a `string` parameter to the first match of a regular expression scanned (not anchored) against the remaining text. If there are capturing groups, `Groups[1].Value` is bound; otherwise the whole match is used. A non-match throws `CommandArgsParseException`.

```csharp theme={null}
[MessageHandler]
[Command(Aliases = ["add"])]
public static async Task<Result> Add(
    Message msg,
    ITelegramBotClient bot,
    [Parse(@"\d+")] string first,
    [Parse(@"\d+")] string second,
    CancellationToken ct)
{
    int result = int.Parse(first) + int.Parse(second);
    await bot.SendMessage(msg.Chat.Id, $"Result: {result}", cancellationToken: ct);
    return Result.Handled();
}
// "/add 12 34" → "Result: 46"
```

> [!NOTE]
>
> `[Parse]` must be applied to a `string` parameter (`CUR009`) and cannot be declared after a `[Rest]` parameter on the same handler (`CUR008`).

## BotFather auto-sync

Commands with a `Description` property are automatically collected into `PolyBotBotFatherSync` and synchronized with BotFather on startup. The generator groups commands by `(Scope, LanguageCode)` and sends one `SetMyCommands` call per unique pair.

> [!NOTE]
>
> Commands without a `Description` still route normally, but are skipped during synchronization — the Bot API rejects empty command descriptions, so pushing one would throw. `CUR021` (warning) reminds you to either set a description or mark the command `IsHidden = true`.

```csharp theme={null}
[MessageHandler]
[Command(
    Aliases = ["help"],
    Description = "Show the help message",
    Scope = CommandScope.AllPrivateChats
)]
public static Task<Result> Help(Message msg) { ... }

// This command routes normally but does NOT appear in the BotFather menu
[MessageHandler]
[Command(Aliases = ["debug"], IsHidden = true)]
public static Task<Result> Debug(Message msg) { ... }
```

> [!NOTE]
>
> Only `'/'`-prefixed commands are synchronized; commands with a custom `Prefix` are always hidden from BotFather regardless of `IsHidden`.

BotFather alias names must match `^[a-z0-9_]{1,32}$`; violations are reported as compile-time error `CUR020`.

## Diagnostics reference

> **CUR002 – \[Command] on a non-Message handler**
>
> **Severity:** Warning
>
> `[Command]` is placed on a handler whose update payload is not a `Message`. The command filter is ignored and the handler routes unconditionally.

> **CUR003 – \[Arg]/\[Rest]/\[Parse] without \[Command]**
>
> **Severity:** Warning
>
> An argument attribute appears on a handler that has no `[Command]`. The parameter is treated as a normal DI dependency.

> **CUR004 – Non-constant Prefix**
>
> **Severity:** Warning
>
> The `Prefix` property is not a compile-time constant. Falls back to `'/'`.

> **CUR005 – \[Arg] type mismatch**
>
> **Severity:** Warning
>
> The `[Arg]` parameter type is not the argument type, `Nullable<T>`, or `object`. The parameter is resolved from DI instead.

> **CUR006 – Multiple \[Rest] parameters**
>
> **Severity:** Warning
>
> More than one `[Rest]` parameter is declared on a handler. Extra parameters beyond the first are treated as ordinary DI parameters.

> **CUR007 – \[Rest] on a non-string parameter**
>
> **Severity:** Warning
>
> `[Rest]` is applied to a parameter whose type is not `string`. The parameter falls back to DI.

> **CUR008 – \[Parse] after \[Rest]**
>
> **Severity:** Warning
>
> A `[Parse]` parameter is declared after a `[Rest]` parameter. The `[Parse]` parameter falls back to DI since `[Rest]` absorbs all remaining text.

> **CUR009 – \[Parse] on a non-string parameter**
>
> **Severity:** Warning
>
> `[Parse]` is applied to a parameter whose type is not `string`. Falls back to DI.

> **CUR020 – Invalid BotFather alias format**
>
> **Severity:** Error
>
> A command alias does not match `^[a-z0-9_]{1,32}$` and cannot be registered with BotFather.

> **CUR021 – Missing Description**
>
> **Severity:** Warning
>
> A `'/'`-prefixed command without `IsHidden = true` has no `Description`. The command is excluded from BotFather sync.

> **CUR022 – Description over 256 characters**
>
> **Severity:** Error
>
> The `Description` string exceeds BotFather's 256-character limit.

> **CUR023 – Duplicate alias in (scope, language) bucket**
>
> **Severity:** Warning
>
> Two commands with the same alias target the same `(Scope, LanguageCode)` pair. The second registration is discarded.

> **CUR052 – Alias starts with the command prefix**
>
> **Severity:** Warning
>
> An alias starts with the same character as the command `Prefix` (e.g. `Aliases = ["/start"]` with the default `/` prefix), so the matcher looks for `//start` and the command can never match. Drop the leading prefix from the alias.

> **CUR046 – GuestMessage handler calls SendMessage**
>
> **Severity:** Warning
>
> A `[GuestMessageHandler]` calls `SendMessage`. Guest messages must be answered with `AnswerGuestQuery` using the message's `GuestQueryId`.

> **CUR047 – GuestMessage handler can answer the guest query more than once**
>
> **Severity:** Warning
>
> Control-flow analysis found a path where `AnswerGuestQuery` executes twice on one update (sequential calls, two calls in one loop body, or one before and one inside a loop). Mutually exclusive `if/else` branches with one call each are not flagged.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.