Skip to content

PR1: feat(telegram): register bot command menu with setMyCommands - #140

Open
rvalitov wants to merge 1 commit into
SamNet-dev:mainfrom
rvalitov:pr/1-setmycommands
Open

rvalitov wants to merge 1 commit into
SamNet-dev:mainfrom
rvalitov:pr/1-setmycommands

Conversation

@rvalitov

@rvalitov rvalitov commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #136

🔢 Merge order

PR 1 of 4 — please merge this one FIRST.

This PR and the three that follow are a stack against main. GitHub requires a
PR's base branch to exist in the target repository, and these branches live in
a fork, so all four target main directly. The diffs will therefore look like
they overlap:
PR 2's diff also contains PR 1's commits, PR 3's contains 1–2,
and so on. This resolves itself automatically as each one merges — no rebasing
is needed. Merging out of order will surface unrelated commits in the diff.

Related PR list:

  1. PR1: feat(telegram): register bot command menu with setMyCommands #140
  2. PR2: feat(telegram): keyboard primitives and update-parser rewrite #141
  3. PR3: feat(telegram): interactive inline-keyboard menus #142
  4. PR4: feat(telegram): traffic history, analytics and an informative periodic report #143

feat(telegram): register the bot command menu with setMyCommands

The problem

The bot has 27 commands, but nothing tells a user they exist. The client-side /
menu button shows an empty list, so the only way to discover a command is to
already know it — or to read /mp_help, which itself has to be known.

That is a poor fit for the public self-service tier in particular, where the
whole point is that an end user with a secret label can check their quota
without an admin walking them through it.

What this does

2026-09-18_11-03-09

Registers command lists with Telegram via setMyCommands, scoped so each role
sees only what it may run:

Scope Contents
Default (everyone) TG_CMDS_PUBLIC/start, /my_status, /redeem, /voucher, /support
Root admin chat + each superadmin in admins.conf TG_CMDS_SUPERADMIN — the admin plane plus /mp_remove, /mp_restart, /mp_update, /mp_lockdown
Each reseller in admins.conf TG_CMDS_ADMIN — the admin plane, minus the superadmin-only four

The scope mirrors the gates _process_cmd already applies, so the menu never
advertises something a role cannot actually run.

Three tables (TG_CMDS_PUBLIC, TG_CMDS_ADMIN, TG_CMDS_SUPERADMIN) are the
single source of truth. The bot daemon does not duplicate them — it re-runs
mtproxymax telegram sync-commands on boot instead.

When it re-syncs

  • On bot service start (the daemon backgrounds a sync-commands before the poll loop)
  • When an admin is granted a role (admin_add) or has one revoked (admin_remove) — revocation calls deleteMyCommands for that chat so a demoted admin stops seeing the admin plane
  • Manually: mtproxymax telegram sync-commands
  • At the end of telegram setup

Every call is best-effort. A Telegram outage must never abort setup or stall the
poll loop, so failures are logged and swallowed.

Defensive details

  • Descriptions are sanitised before being embedded in JSON (control characters
    stripped, quotes and backslashes escaped). Telegram rejects the entire list
    if any single entry is malformed, so one stray character would blank the whole
    menu.
  • _tg_api_post keeps the bot token out of the process list by writing the URL
    into a curl -K config file rather than passing it as an argument.

Testing

tests/test_telegram_commands.sh28 assertions, 0 failures.

It stubs curl to capture method | chat_id | commands into a TSV and asserts:
scoping per role, no duplicate commands across scopes, that the emitted bodies
are valid JSON, and that a network failure is survivable rather than fatal.

$ bash tests/test_telegram_commands.sh
28 tests, 0 failures

Files

  • mtproxymax.sh — command tables, JSON rendering, sync/clear, CLI sync-commands, wizard and boot hooks
  • tests/test_telegram_commands.sh — new
  • README.md — documents the / menu and telegram sync-commands

The bot never registered its commands, so Telegram's in-app "/" menu button
had nothing to show and the commands were only discoverable by reading
/mp_help. Register the list with setMyCommands so commands are tappable.

Lists are scoped to mirror the role model in _process_cmd rather than
exposing the whole admin surface to every user:

  - default scope   -> 5 public self-service commands
  - admin/superadmin chats via admins.conf -> 18 command control plane
  - root chat and superadmin admins -> those 18 plus the four commands
    gated on the superadmin role (/mp_remove, /mp_restart, /mp_update,
    /mp_lockdown)

The command tables live once, in the manager. The generated bot daemon
re-runs `mtproxymax telegram sync-commands` on boot instead of carrying its
own copy, so the menu self-heals and picks up newly added admins. Syncing is
best-effort throughout: a Telegram outage must never break setup or the poll
loop, and revoking an admin calls deleteMyCommands so a stale admin menu is
not left behind.

Also add an explicit `telegram sync-commands` subcommand for manual re-sync.
@rvalitov rvalitov changed the title feat(telegram): register bot command menu with setMyCommands PR1: feat(telegram): register bot command menu with setMyCommands Sep 17, 2026
@rvalitov
rvalitov marked this pull request as ready for review September 18, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bot commands are undiscoverable without already knowing them

1 participant