Skip to main content
Glama
README.md
# tgx 1.0.0

Telegram из терминала: полноэкранный TUI + скриптовый CLI на Telethon,
плюс коннектор MCP, через который тем же самым пользуется агент.

Что менялось — в [CHANGELOG.md](CHANGELOG.md).

```bash
tgx ui              # полноэкранный клиент
tgx ui --demo       # то же самое на демо-данных, без входа в аккаунт
tgx                 # карта команд с анимированной заставкой
tgx dialogs --jsonl # машинный вывод для скриптов (как раньше)
```

## Установка

```bash
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
```

`bin/tgx` — обёртка, запускающая `bin/tgx.py` из `.venv`. Ключи API берутся из
`data/config.json` или переменных `TG_API_ID` / `TG_API_HASH`; сессия лежит в
`data/tgx.session`. Если сессия разлогинена, `tgx ui` сам покажет экран входа
(телефон → код → облачный пароль).

## TUI

Слева — список чатов с непрочитанными, превью и папками Telegram; справа —
переписка баблами с разделителями дат, ответами и картинками прямо в бабле;
снизу — поле ввода. Новые сообщения прилетают вживую (Telethon events) и появляются с
плавным проявлением, чужие чаты дают всплывающее уведомление.

| клавиша | действие |
|---|---|
| `/` | фильтр по списку чатов |
| `ctrl+k` | фокус на список чатов |
| `↑ ↓` / `j k` | навигация по списку и по сообщениям |
| клик по сообщению | выделить его (дальше работают `v`, `o`, `ctrl+r`) |
| `enter` | открыть чат · отправить сообщение |
| `p` | редактор поста: разметка, живое превью, файл, отложенная публикация |
| `l` | чек-лист: создать, отмечать пункты, дописывать |
| `t` | темы форума: переключение, создание, закрытие, закрепление |
| `shift+p` | закрепить или открепить сообщение (тихо) |
| `s` | показать/скрыть спойлер в сообщении |
| `ctrl+s` | прикрепить файл: фото, видео, кружок, голосовое, документ, альбом |
| `c` | комментарии к посту канала — читать и писать |
| `ctrl+f` | поиск внутри чата |
| `ctrl+r` | ответить на выбранное сообщение |
| `ctrl+y` | скопировать текст сообщения |
| `e` / `x` | править · удалить своё сообщение (удаление спросит подтверждение) |
| `f` | переслать сообщение в другой чат |
| `+` / `−` | поставить · убрать реакцию |
| `b` | нажать инлайн-кнопку бота |
| `v` / `o` | картинка на весь экран · открыть в системном просмотрщике |
| `ctrl+d` | скачать вложение целиком в `data/downloads` |
| `ctrl+e` | многострочный редактор |
| `ctrl+u` | догрузить историю выше |
| `ctrl+n` | следующий непрочитанный чат |
| `ctrl+b` | свернуть боковую панель (режим фокуса) |
| `shift+r` | отметить чат прочитанным |
| `ctrl+t` | следующая тема · `ctrl+p` — палитра команд |
| `f1` / `?` | справка · `f12` — SVG-скриншот |
| `ctrl+q` | выход |

Чаты **не** помечаются прочитанными автоматически — только по `shift+r` или с
флагом `--mark-read`.

Флаги: `--theme tgx-night|tgx-day|nord|gruvbox|catppuccin-mocha`, `--demo`,
`--mark-read`, `--no-notify`, `--no-splash`, `--effect <имя>`, `--limit N`.

## Список чатов и папки

Грузятся **все** чаты, а не первая страница: первые 200 показываются сразу, чтобы
интерфейс не ждал, остальные догружаются в фоне и вливаются в список (в строке
над полем ввода видно «догружаю остальные чаты…»). Ограничить можно флагом
`--limit N`, `0` — без ограничения, это и есть значение по умолчанию.

Папки Telegram применяются целиком, со всеми правилами `DialogFilter`, а не
только со списком явно добавленных чатов:

| правило папки | учитывается |
|---|---|
| явно добавленные и закреплённые чаты | да |
| явно исключённые чаты | да, исключение сильнее всего |
| категории: контакты, не-контакты, группы, каналы, боты | да |
| «исключить прочитанные / без звука / архив» | да |

Именно поэтому папка вида «все группы» раньше выглядела почти пустой — учитывался
только явный список.

## Чек-листы

Telegram умеет чек-листы прямо в сообщении, и tgx тоже. **`l`** на пустом месте
создаёт новый (заголовок и по пункту на строку), а на сообщении с чек-листом —
открывает его: `enter` отмечает или снимает пункт, поле снизу дописывает новые.
В бабле список виден целиком, с `☐`/`☑` и счётчиком выполненного.

Отмечать и дописывать могут другие участники — это два переключателя при
создании. Операции возвращают состояние **с сервера**, поэтому интерфейс не
расходится с тем, что видят остальные.

```bash
tgx todo @chat "План недели" "созвон" "макеты" "релиз"
tgx todo-check @chat 4321 --done 1,2 --undone 3
tgx todo-add @chat 4321 "ретро"
```

## Расшифровка голосовых

```bash
tgx transcribe status                    # доступна ли и сколько бесплатных осталось
tgx transcribe get @чат 295429           # голосовое или кружок → текст
tgx transcribe rate @чат 295429 3874… good
```

В TUI — `a` на выбранном голосовом; расшифровка появляется в самом пузыре
курсивом, а под голосовыми и кружками висит подсказка `a — расшифровать`.

Главная тонкость [документации](https://core.telegram.org/api/transcribe)
незаметна снаружи: ответ на `messages.transcribeAudio` почти всегда приходит с
флагом `pending` и пустым текстом, а готовая расшифровка прилетает позже
апдейтом `updateTranscribedAudio`. Подписка на него ставится **до** запроса —
иначе быстрый ответ проскакивает мимо, и команда молча возвращает пустую строку.
Если за `--wait` секунд текст не пришёл, отдаётся то, что успело, с пометкой
`pending`.

Без Premium Telegram даёт `transcribe_audio_trial_weekly_number` расшифровок в
неделю, каждая не длиннее `transcribe_audio_trial_duration_max` секунд; в
супергруппе с бустом от `group_transcribe_level_min` они не тратятся. `status`
показывает эти числа для вашего аккаунта, а остаток — `free_left` в ответе.

## Звонки

```bash
tgx call start @группа --title "Планёрка"
tgx call participants @группа / info / join-as
tgx call invite @группа @кто / mute @группа @кто --volume 150
tgx call record @группа --video / title @группа "Новое имя"
tgx call settings @группа --join-muted on / say @группа "начинаем"
tgx call stream-url @группа        # адрес и ключ для внешней трансляции
tgx call watch @группа --open      # живая страница участников
tgx call end @группа --confirm-to me --as @бот
```

Звук идёт по WebRTC, и на чистом Python его не сыграть — **зато всё вокруг
звука делается обычными вызовами**. Поэтому разделение честное: терминал
распоряжается звонком, а подключиться со звуком можно в приложении по ссылке.

**`watch` поднимает маленькую страницу** на `127.0.0.1` и обновляет её сама:
видно, кто говорит, кто заглушён и кто поднял руку. Терминалу такое
перерисовывать нечем — таблица мигала бы всем экраном. Страница наружу не
смотрит и ничего не сохраняет, а данные берёт у того же процесса, что их
опрашивает: второго соединения с Telegram не появляется.

Ссылку на звонок выдают только публичные чаты — у приватного её не существует,
и `watch` это переживает, показывая участников без неё.

## Сессии и приватность

```bash
tgx security sessions / websites          # устройства и сайты
tgx security close-session HASH --confirm-to me --as @бот
tgx security privacy                      # кто что о вас видит
tgx security set-privacy last-seen contacts --deny @кто
tgx security global-privacy --hide-read on
tgx security session-ttl 7 / account-ttl 548
tgx security notify-exceptions
```

**Сессии и сайты — две разные породы.** Обычные входы и входы через «Войти в
Telegram» на сайтах сбрасываются разными методами, и сброс одного не трогает
другой; отсюда две команды, а не одна.

**Приватность Telegram хранит парами правил**: «только контактам» — это
«разрешить контактам» плюс «запретить всем остальным». Напечатанные подряд, они
читаются как противоречие — «контактам, никому», — поэтому сводка выбирает одну
основу и дописывает к ней исключения: `контактам · кроме 3 выбранных`. По этой
строке человек решает, что видно посторонним, и ошибка тут стоит дорого.

Основа (`everyone`/`contacts`/`nobody`) при изменении обязательна: правило без
неё молча оставило бы прежнюю аудиторию.

## Черновики, отложенные, заготовки, избранное

```bash
tgx pending drafts                    # все черновики; они видны только вам
tgx pending draft @чат "текст"        # пустой текст стирает черновик
tgx pending scheduled @чат            # что уйдёт само и когда
tgx pending send-now @чат 123 / cancel @чат 123
tgx pending shortcuts / shortcut 1    # быстрые ответы и их содержимое
tgx pending send-shortcut @чат 1
tgx pending saved / tags / name-tag 🔥 "Важное"
tgx pending fact-check @канал 123
```

Telegram держит порознь четыре разных способа отложить сообщение, и команды
повторяют это различие:

* **черновик** живёт в чате, виден только вам и **не отправится сам никогда**;
* **отложенное** — уже полноценное сообщение с назначенным временем: оно уйдёт
  само, и «отменить» значит удалить его до срока;
* **быстрый ответ** — заготовка под ярлыком, которую отправляют вручную; именно
  на неё ссылаются приветствие и автоответ бизнес-режима;
* **избранное** — пересланное себе, разложенное по авторам.

Черновик **перезаписывается целиком**, поэтому «дописать» его нельзя: сперва
читаем, потом сохраняем всё вместе. Время принимается и полное
(`2026-09-01T10:00`), и относительное (`+30m`, `+2h`, `+3d`); прошедшее
отвергается сразу, а не после отправки.

## Стикеры и статистика

```bash
tgx stickers show HotCherry                    # что внутри набора
tgx stickers suggest "Мой набор" --as @бот     # подобрать свободное имя
tgx stickers create me "Мой набор" myset a.png=😀 b.png=😎 --as @бот
tgx stickers add myset c.png 🔥 --as @бот
tgx stickers move myset 3 0 / emoji myset 0 🎉 / thumb myset 0 --as @бот

tgx stats channel @канал / group @группа
tgx stats message @канал 123 / story @кто 490
tgx stats forwards @канал 123                  # кто публично переслал
tgx stats graph @канал views_graph             # догрузить один график
```

**Наборы правит бот, владельцем остаётесь вы.** Telegram требует указать
владельца, а вызывает метод бот — поэтому у команд правки обязателен `--as`,
и это не прихоть, а условие API.

**Стикер адресуется документом, а не номером.** Номер живёт до первой
перестановки, поэтому команды принимают позицию, но переводят её в документ
прямо перед вызовом — иначе после `move` следующая правка попала бы не в тот
стикер.

**Статистика приходит наполовину токенами.** Числа читаются сразу, а графики —
это токены, которые надо обменять отдельным вызовом. Поэтому `channel` печатает
список доступных графиков, а `graph` догружает один по имени: иначе одна
команда тянула бы десяток запросов.

## Общие папки

```bash
tgx share-folder share 24 "Рабочее"      # выписать ссылку на набор чатов
tgx share-folder invites 24 / revoke 24 СЛАГ
tgx share-folder check СЛАГ              # что внутри чужой ссылки — до принятия
tgx share-folder join СЛАГ
tgx share-folder updates 24 / accept 24  # что автор добавил с прошлого раза
tgx share-folder leave 24 --confirm-to me --as @бот
```

**Личные переписки в общую папку не отдаются** — Telegram их не делит, поэтому
`share` без списка чатов берёт только делимые, а если таких нет, говорит об этом
вместо пустой ссылки. И поделиться можно лишь папкой, созданной как общая:
обычная отвечает `FILTER_NOT_SUPPORTED`.

Принятая папка живёт дальше: автор добавляет чаты, а у вас появляются
обновления — их принимают (`accept`) или прячут (`hide-updates`).

## Истории

```bash
tgx stories feed                       # лента; --hidden — скрытые
tgx stories of @кто / pinned / archive
tgx stories publish фото.jpg --caption "…" --audience close --hours 24
tgx stories viewers 490                # кто смотрел и как отреагировал
tgx stories react @кто 332 ❤ / read @кто 332
tgx stories stealth                    # просмотры не засчитываются (Premium)
tgx stories albums / new-album "Лето" 464 465
tgx stories search хештег / link @кто 332 / hide @кто
tgx stories delete 490 --confirm-to me --as @бот
```

**Приватность по умолчанию — близкие друзья, а не «всем».** Аудитория задаётся
списком правил, которые складываются: `--audience contacts --deny @кто`
означает «контактам, кроме него». Промахнуться в сторону «всем» дороже, чем в
сторону «слишком узко», поэтому умолчание выбрано самым узким.

**Просмотр чужой истории виден автору.** Поэтому чтение ленты и отметка о
просмотре — разные команды: `feed` ничего не отмечает, `read` отмечает и прямо
говорит об этом. Скрытный режим (`stealth`) существует ровно затем, чтобы
просмотр не засчитался; он требует Premium.

Срок жизни — 6, 12, 24 или 48 часов; `--pin` оставляет историю в профиле после
срока.

## Контакты и чёрный список

```bash
tgx contacts list / search "имя" / by-phone +7…
tgx contacts add @кто --note "познакомились на конференции"
tgx contacts remove @кто
tgx contacts block @кто / unblock @кто
tgx contacts blocked --stories        # кому запрещены истории
tgx contacts close-friends @а @б      # список задаётся целиком
tgx contacts birthdays / top / import ССЫЛКА
```

Telegram различает три вещи, которые легко спутать, и tgx повторяет это
различие вместо того, чтобы прятать:

* **Контакт** — запись в адресной книге. `remove` убирает её, но писать вам
  человек по-прежнему сможет; ответ команды об этом прямо говорит.
* **Блокировка** — запрет писать. У неё есть второй, независимый список:
  `--stories` управляет тем, кому не видны ваши истории.
* **Близкие друзья** — отдельная отметка, влияющая только на истории; список
  задаётся целиком, а не по одному человеку.

Заметка к контакту (`--note`) видна только вам, но хранится у Telegram — это
часть аккаунта, а не локальный файл. И свой номер при добавлении по умолчанию
не раскрывается: для этого есть отдельный `--share-phone`.

## Человек в контуре

Опасное не запрещено — оно спрашивает. Бот присылает карточку с описанием того,
что сейчас произойдёт, и двумя кнопками; действие ждёт нажатия.

```bash
tgx confirm "Удалить 40 сообщений?" --details "Чат: Команда" \
    --danger "необратимо" --to me --as @мой_бот --timeout 300
```

Возвращает решение и завершается с кодом 2, если согласия не было, — сценарий
останавливается сам. Тем же ключом закрыты необратимые команды:

```bash
tgx delete @чат 10 11 12 --yes --confirm-to me --as @мой_бот
tgx guard check @чат --confirm-to me --as @мой_бот
tgx pay send "https://t.me/$…" --as @мой_бот --confirm-to me
```

Две вещи, без которых подтверждение было бы декоративным:

* **Нажать может только тот, кого спросили.** Кнопки видны всем в чате, поэтому
  автор нажатия сверяется с адресатом; чужое нажатие отклоняется, ему
  показывается пояснение, а сам факт попадает в ответ полем `strangers`.
* **Ключ одноразовый.** У каждого запроса свой случайный ключ, живущий до
  первого решения или до истечения срока; повторное нажатие ничего не меняет.

Опрос идёт через `getUpdates`, поэтому у бота не должно быть вебхука и второго
получателя обновлений — иначе нажатие уйдёт не сюда.

## Платежи: звёзды, TON и счета

Покрыто **62 метода `payments` из 66**. Не взяты четыре: покупки через
App Store и Play Market (нужен чек от магазина, с настольного клиента их
не сделать) и служебный `TL`.

```bash
tgx pay balance                 # баланс звёзд
tgx pay balance --ton           # он же в TON
tgx pay history --outbound      # история операций
tgx pay receipt @чат 123        # чек по оплаченному сообщению
tgx pay show "https://t.me/$…"  # что просит счёт — без оплаты
tgx pay invoice "Название" "Описание" --price "Строка=50" --as @бот
tgx pay gift-options @друг
```

Три валюты живут в одних и тех же вызовах и различаются флагом: `getStarsStatus`
с `ton=True` отвечает про криптовалюту, и так же устроена история операций.

Три вещи, на которых легко ошибиться:

* **Звёзды целые.** `--price "Донат=50"` — это пятьдесят звёзд. Обычные валюты
  Telegram принимает в сотых (`12.5` USD → `1250`), и если применить это правило
  к звёздам, счёт выписывается в сто раз больше. Заметно только у плательщика.
* **Счета выписывает бот.** От лица пользователя `payments.exportInvoice`
  отвечает `USER_BOT_REQUIRED`, поэтому `--as @бот` обязателен. Звёздам
  (валюта `XTR`) платёжный провайдер не нужен — Telegram сам платёжная система;
  для остальных валют нужен токен провайдера.
* **Сумма операции лежит в поле `amount`,** а не `stars` — на `stars` приходят
  сплошные нули, и история выглядит пустой.

Что ещё умеет `pay`:

```bash
tgx pay gifts                      # каталог подарков Telegram и цены
tgx pay my-gifts                   # полученные подарки
tgx pay subscriptions              # подписки за звёзды
tgx pay revenue @канал --ton       # сколько заработал канал
tgx pay check-code СЛАГ            # что даёт подарочный код — до применения
tgx pay giveaway @канал 123        # сведения о розыгрыше
tgx pay saved-info                 # что Telegram хранит из платёжных данных
```

Тратящее и необратимое — только через подтверждение человеком:

```bash
tgx pay convert-gift 185532 --confirm-to me --as @бот   # подарок → звёзды
tgx pay transfer-gift 185532 @друг --confirm-to me --as @бот
tgx pay react @канал 42 5 --confirm-to me --as @бот     # платная реакция
tgx pay apply-code СЛАГ --confirm-to me --as @бот
tgx pay cancel-subscription @канал ID --confirm-to me --as @бот
tgx pay clear-saved --confirm-to me --as @бот
```

Без `--confirm-to` такие команды отказываются работать — это не флаг вежливости,
а условие запуска.

Подарки — от витрины до вторичного рынка:

```bash
tgx pay gift 185532                  # подробности своего подарка
tgx pay upgrade-preview ID           # что даст улучшение — до оплаты
tgx pay resale ID                    # кто что продаёт и почём
tgx pay unique СЛАГ                  # уникальный подарок по ссылке
tgx pay collections / new-collection # коллекции на витрине
tgx pay show-gift 185532 --hide      # спрятать из профиля
tgx pay pin-gift 185532              # закрепить наверху
tgx pay topup                        # пакеты звёзд и цены
tgx pay auctions / referrals / can-send-gift ID
```

С воротами подтверждения: `upgrade-gift`, `sell-gift` (выставить цену или снять),
`refund` (вернуть звёзды за покупку).

**Цена приходит списком.** Одна и та же вещь стоит и звёзд, и TON: `460 ⭐` —
это `5.07 TON`. При этом у TON нет поля `nanos`: вся сумма записана в `amount`
в нанотонах, и если поделить её как звёзды, `4.68 TON` превращаются в
`4 680 000 000`.

Аукционы, крафт, партнёрские программы и прочее:

```bash
tgx pay auction-state ID / auction-won ID
tgx pay craftable ID                  # из чего собрать подарок
tgx pay suggested-referrals           # какие боты предлагают партнёрство
tgx pay referral @бот                 # условия и заработок по одной программе
tgx pay premium-options               # почём подарить Premium
tgx pay giveaway-options              # варианты розыгрышей звёзд
tgx pay unique-value СЛАГ             # во что оценивается уникальный подарок
tgx pay upgrade-attributes ID         # варианты оформления и их редкость
tgx pay transaction ID…               # операции по идентификаторам
tgx pay ads-account @канал
tgx pay validate-info ССЫЛКА --name … # проверить данные ДО списания
```

За воротами: `craft`, `offer`, `answer-offer`, `connect-referral`,
`revoke-referral`, `delete-collection`, `fulfil-subscription`,
`gift-to-blockchain`, `pay-card`.

**Оплата картой — без ввода карты.** Telegram хранит способ оплаты у себя;
`tgx pay pay-card` берёт временный пароль в обмен на пароль от аккаунта (по SRP)
и платит сохранённым способом. Номер карты не проходит ни через аргументы, ни
через этот процесс. Первый платёж картой всё равно делается в приложении — там
способ и сохраняется.

Секреты — скрытым вводом, не аргументом:

```bash
tgx pay withdraw @канал 100 --confirm-to me --as @бот
tgx pay card-bank
```

Оба спрашивают секрет в скрытой строке: он не появляется ни в аргументах, ни в
истории оболочки, ни в списке процессов. Для автоматизации есть переменные
`TGX_PASSWORD` и `TGX_CARD_NUMBER`; без терминала и без переменной команда
честно отказывает, а не зависает в ожидании ввода, которого никто не сделает.

* **Вывод** сперва спрашивает согласие кнопкой, потом пароль. Пароль на сервер
  не уходит: Telegram проверяет его по SRP, то есть по доказательству знания.
  Денег команда не переводит — возвращает ссылку на страницу вывода, где
  подтверждение происходит уже у человека.
* **Банк по карте** требует полный номер: на первых шести цифрах сервер
  отвечает отказом. Справочник банков лежит в другом дата-центре, и запрос
  повторяется там, куда указал сервер.

**Оплата счёта — тоже за воротами подтверждения.** `tgx pay send` сначала
перечитывает счёт, показывает сумму человеку карточкой с кнопками и списывает
звёзды лишь после «Разрешить». Форма запрашивается заново перед списанием:
между показом суммы и согласием проходит время, а платить надо ровно за то, что
человек видел. Карты отсюда не оплачиваются — платёжные данные вводят на своём
экране.

## Эффекты и свёрнутые цитаты

```bash
tgx effects --search 🎉        # 697 эффектов: эмодзи и id
tgx send @друг "с праздником" --effect 5046509860389126442
```

Из Bot API 7.4. **Эффект работает только в личной переписке** — в группе и
канале сервер отвечает `EFFECT_CHAT_INVALID`, поэтому tgx отказывает заранее и
объясняет почему.

Оттуда же свёрнутые цитаты во входящих: в терминале сворачивать нечего, но
такая врезка помечается уголком `▾` вместо обычной полосы `▌` — иначе длинная
цитата выглядит обычным текстом, которым автор её не считал.

## Копии и бусты

```bash
tgx copy @откуда @куда 123 124 --drop-captions --topic 2
tgx boosts status @канал     # уровень и сколько до следующего
tgx boosts who @канал        # кто бустил
tgx boosts mine              # свои слоты
tgx boosts give @канал
```

`copy` — это `copyMessage` из Bot API: тот же метод, что и пересылка, но с
`drop_author`, поэтому получатель не видит источник и сообщение не тянет за
собой ссылку на него.

## Обложка и точка старта видео

```bash
tgx send @чат "подпись" --file ролик.mp4 --cover обложка.png --start-at 12
```

Из Bot API 8.3. Telethon не пробрасывает `video_cover` и `video_timestamp`
через `send_file`, поэтому медиа собирается вручную, а обложка сперва
загружается как фотография.

**У ролика без звуковой дорожки этого не будет.** Telegram добавляет такому
файлу `DocumentAttributeAnimated`, обращается с ним как с гифкой, и оба поля
молча пропадают — видно только на живом сообщении.

## Дата и время в тексте

Сущность `date_time` (Bot API 9.5, в слое MTProto — `MessageEntityFormattedDate`)
хранит момент времени, а не написанную кем-то строку: каждый клиент показывает
его по-своему. tgx подставляет момент по флагам автора — день недели, короткая
или длинная дата, время с секундами или без.

## Опросы и викторины

```bash
tgx poll create @чат "Вопрос?" "А" "Б" --multiple --members-only --countries RU,DE
tgx poll create @чат "Вопрос?" "А" "Б" --quiz 0 --as @бот --explanation "почему А"
tgx poll vote @чат 36 0            # без вариантов — снять голос
tgx poll results @чат 36
tgx poll voters @чат 36 --option 0 # если опрос не анонимный
tgx poll close @чат 36
```

Из Bot API 10.0: одного варианта теперь достаточно (раньше требовалось два),
`--members-only` ограничивает опрос подписчиками, `--countries` — списком стран.
Из 9.6: правильных ответов в викторине может быть несколько (`--quiz 0 2`),
появилось `--allow-revoting`, а срок автозакрытия вырос с суток до месяца.

**Викторина требует `--as @бот`.** В схеме `inputMediaPoll.correct_answers` —
вектор байтовых строк, а Telethon 1.44 объявляет и пакует его как вектор чисел,
из-за чего сервер отвергает любое значение. Через Bot API правильный ответ
задаётся обычным номером, поэтому викторина уходит оттуда. Обычные опросы этим
не задеты и идут по MTProto от вашего имени.

## Кнопки, вложения и богатые сообщения

```bash
tgx rich @чат --file пост.md --topic 2 --media banner=баннер.png     # от своего имени
tgx bot rich @чат --as @мой_бот --file пост.md --topic 2 \
    --media banner=баннер.png --button "Сайт[primary]=https://…"      # от бота, с кнопками
```

Три вещи, которые стоят за этими флагами:

* **Кнопки вешает только бот.** Обычному аккаунту сервер `reply_markup` принимает
  без ошибки и молча выбрасывает — ни у простого сообщения, ни у богатого кнопок
  не появится. Проверять надо чтением сообщения обратно, а не отсутствием
  исключения.
* **Стили кнопок** появились в слое 227: `Текст[primary]=…`, `[danger]`,
  `[success]`, а `[success:5312…]` ставит эмодзи вместо галочки.
* **Блочная форма (10.2–10.3)** — единственный способ положить кнопки и файлы
  *внутрь* документа: `tgx bot rich --blocks файл.json --attach имя=путь`.
  Сборщики блоков лежат в `tgx_rich`: `heading`, `quote(expandable=True)`,
  `table(compact=True)`, `button_row`, `document_block`. Отправка работает, а вот
  **показать такое сообщение tgx пока не может**: конструкторы блочной формы
  новее слоя MTProto, который знает Telethon, и оно приходит как «сообщение
  нового типа». Получатель с обновлённым клиентом видит документ полностью.
* **Картинка внутрь документа.** Bot API берёт медиа либо по публичному URL, либо
  частью формы, поэтому файл с диска уходит как `attach://` вместе с multipart —
  это единственный способ, когда репозиторий приватный и публичной ссылки нет.
  Если ссылки `tg://photo?id=…` в тексте нет, tgx ставит картинку в начало сам:
  иначе Telegram молча выбросит вложение.

## Именные приглашения

```bash
tgx guard lock @чат                        # звать людей могут только администраторы
tgx guard invite @чат @кого --note "зачем" # ссылка на одного, одно использование
tgx guard check @чат                       # вошёл не тот — удаляем, ссылку отзываем
tgx guard journal
```

Ссылку легко переслать, поэтому сверка идёт по факту входа, а не по обещанию:
журнал (права 600) помнит, кому она выписана, а `check` сравнивает это с тем, кто
реально вошёл. Лишний удаляется баном с мгновенным разбаном — без вечного бана.
Решение «выгнать» вынесено в отдельную функцию и покрыто тестами, чтобы не
проверяться на живых людях.

## Форумы и темы

Тема — это не папка, а тред служебного сообщения, которым её создали: id темы
равен id этого сообщения. Отсюда все особенности, и tgx закрывает
[core.telegram.org/api/forum](https://core.telegram.org/api/forum) целиком:

```bash
tgx forum on @группа --tabs on      # включить форум (только владелец)
tgx forum topics @группа            # список; --search ищет по названию
tgx forum icons                     # иконки, доступные без Premium
tgx forum create @группа "Релизы" --color зелёный
tgx forum create @группа "Идеи" --emoji 5312536423851630001
tgx forum edit @группа 4 --title "Архив" --close
tgx forum pin @группа 2
tgx forum reorder @группа 3 2 --force
tgx forum show @группа 4            # так же узнают, что тему удалили
tgx forum delete @группа 4 --yes    # необратимо: уносит всю переписку
tgx forum as-messages @группа on    # показывать форум сплошной лентой
```

Правила Telegram зашиты в команды, а не оставлены на память:

* **«Общую» тему (id 1) нельзя удалить** — только скрыть; и скрыть можно
  единственно её, остальные закрывают.
* **Цвет стандартной иконки выбирается один раз при создании** — у
  `editForumTopic` такого поля просто нет.
* **Переименование и закрытие — разные запросы.** На попытку сделать это одним
  сервер отвечает `TOPIC_CLOSE_SEPARATELY`; `tgx forum edit` разбивает вызов сам.
* **Иконка-эмодзи без Premium — только из стандартного набора**, его печатает
  `tgx forum icons`.
* **Закрепить можно `topics_pinned_limit` тем** (у аккаунта — 5); `reorder`
  проверяет это до отправки.
* **Об удалении темы отдельного апдейта нет** — пропадает корневое сообщение, а
  `tgx forum show` возвращает по этому id `{"deleted": true}`.

В TUI на экране тем добавлено `d` — удаление с подтверждением вторым нажатием.

## Оформление профиля

Аватар в Telegram — уже не «одна картинка»: тем же полем ставятся четыре разные
вещи, и одинаково для себя, канала, бота и контакта.

```bash
tgx profile formats                      # шпаргалка по форматам
tgx profile photo фото.png               # статичный аватар
tgx profile photo клип.mp4 --start 2.5   # видеоаватар; --start выбирает обложку
tgx profile photo emoji:5366316836101038579 --colors "#229ED9,#17212B"
tgx profile photo sticker:набор:42       # стикер на градиенте

tgx profile photo лого.png --chat @канал         # аватар канала или группы
tgx profile photo лого.png --bot @my_bot         # аватар своего бота
tgx profile photo лого.png --contact @друг       # фотография контакта у вас
tgx profile photo лого.png --contact @друг --suggest   # предложить её ему
tgx profile photo лого.png --fallback            # публичный запасной аватар

tgx profile photos                       # что у вас стоит: фото, видео, эмодзи
tgx profile emojis --kind status         # эмодзи с их id, чтобы было что выбрать
tgx profile color --color 5 --emoji 5379748062124056162
tgx profile status --emoji 5413694143601842851 --until 1780000000
tgx profile birthday 14.03.1990
tgx profile personal-channel @мой_канал
```

Не квадратное или слишком длинное видео tgx замечает до отправки и предлагает
`--square` и `--trim` — они обрезают через ffmpeg, а не оставляют это Telegram.

### Заставка в аватар

Анимированный баннер tgx можно записать прямо из терминала и поставить аватаром.
Кадры снимаются с настоящего псевдотерминала и разбираются эмулятором, так что в
видео попадает ровно то, что видно в окне, — вместе с блочными глифами и
градиентом:

```bash
tgx profile banner --bot @my_bot                 # записать и поставить
tgx profile banner --chat @канал --effect decrypt
tgx profile banner --save баннер.mp4 --effect matrix --speed 40
```

Обложка выставляется сама на тот момент, когда логотип уже собран, — иначе в
профиле висел бы пустой первый кадр. Цель указывается явно (`--bot`, `--chat`,
`--me`, `--save`): собственный аватар слишком легко сменить по ошибке.

## Бот-секретарь (Telegram Business)

Бот, подключённый к личному аккаунту, читает и отвечает в ваших приватных чатах.
Настраивается это со стороны аккаунта, поэтому живёт в tgx, а не в боте:

```bash
tgx bot secretary @my_bot on           # без этого Telegram ответит BOT_BUSINESS_MISSING
tgx business bots                      # кто уже подключён и с какими правами
tgx business connect @my_bot --rights reply,read_messages --chats all --exclude @boss
tgx business pause @chat               # приостановить бота в одном чате
tgx business disconnect @my_bot

tgx business quick-replies             # быстрые ответы и их id
tgx business greeting --shortcut 1 --after-days 7
tgx business away --shortcut 3 --schedule outside
tgx business hours "пн-пт 9:00-18:00; сб 10:00-14:00" --timezone Europe/Moscow
tgx business intro --title "Привет" --text "Отвечаю в течение дня"
tgx business link "Здравствуйте! Чем помочь?" --title "Поддержка"
```

Права выдаются поштучно: `reply`, `read_messages`, `delete_sent_messages`,
`delete_received_messages`, `edit_name`, `edit_bio`, `edit_profile_photo`,
`edit_username`, `view_gifts`, `sell_gifts`, `change_gift_settings`,
`transfer_and_upgrade_gifts`, `transfer_stars`, `manage_stories` — или `all`/`none`.
Область: `all`, `contacts`, `non-contacts`, `existing`, `new`, плюс `--exclude`.

Два подводных камня, оба проверены на живом аккаунте, ни один не описан в документации:

* **Бот должен сам разрешить это.** В BotFather переключатель называется
  «Secretary Mode» (в API — бизнес-режим); пока он выключен, подключение падает
  с `BOT_BUSINESS_MISSING`. `tgx bot secretary` щёлкает его за вас.
* **Подключённый бот всегда один.** `account.getConnectedBots` возвращает вектор,
  и ограничения в документации нет — но второй бот молча отключает первого.
  Поэтому `connect` отказывается затирать чужое подключение без `--replace`
  и называет того, кого собирается вытеснить.

Со стороны бота это `business_connection_id` в Bot API: он получает апдейты
`business_message` и отвечает от вашего имени. Нужен Telegram Business (Premium).

**Ничего из этого не отдано агентам** — подключение бота к личной переписке
решает человек, не модель.

## Богатые сообщения (Bot API 10.1)

Отдельный вид сообщения из Bot API 10.1 (11 июня 2026), дополненный в 10.2: не
текст с разметкой, а **документ** — заголовки шести уровней, таблицы, списки
задач, сноски, раскрывающиеся блоки, формулы, галереи, блок «размышления».

Отправлять их можно двумя путями, и tgx умеет оба:

- **от своего аккаунта** — по MTProto (`messages.sendMessage(rich_message=…)`,
  слой 227, Telethon 1.44). Ограничение «только боты» относится к Bot API, а не
  к протоколу;
- **от имени бота** — по HTTP Bot API, с кнопками под сообщением и потоковым
  черновиком `sendRichMessageDraft`.

Входящие богатые сообщения клиент **отрисовывает в терминале**: заголовки,
списки, таблицы, цитаты, код, спойлеры и сноски. Внутри это дерево блоков
Instant View (`PageBlock*`), поэтому рендер общий с обычной разметкой.

```bash
tgx bot rich-syntax                      # шпаргалка по разметке
tgx bot rich @channel --as @my_bot --file post.md \
  --button "Открыть=webapp:https://app.example.com"
tgx bot rich @channel --as @my_bot --file post.md --draft   # потоковый черновик
tgx bot rich @channel --as @my_bot --file post.md \
  --media cover=https://example.com/cover.jpg   # для ![](tg://photo?id=cover)
```

Разметка (`markdown`-поле `InputRichMessage`):

```
# Заголовок 1 … ###### Заголовок 6      **жирный** *курсив* ~~зачёркнутый~~
==выделенный==   ||спойлер||   `код`    ```python … ```   $x^2 + y^2$
- пункт   1. нумерованный   - [ ] задача   - [x] сделано
> цитата
| Заголовок | Ещё |    ![](https://…/photo.jpg "подпись")    Сноска[^1]
|:----------|:---:|    ![](tg://photo?id=cover)             [^1]: текст
```

Лимиты Telegram проверяются локально, до запроса, и объясняются по-человечески:
32768 символов, 500 блоков, 16 уровней вложенности, 50 вложений, 20 колонок в
таблице. Ссылки вида `tg://photo?id=…` должны быть перечислены в `--media`,
иначе tgx откажет заранее.

В TUI это переключатель **«богатое»** в редакторе поста (`p`) рядом с выбором
бота: без бота редактор предупредит, потому что личный аккаунт такие сообщения
слать не умеет. Агентам добавились `rich_syntax` (чтение) и `bot_send_rich`
(запись).

Не путать с соседним разделом: `article` публикует **страницу на telegra.ph** и
кидает ссылку, а `bot rich` отправляет **само сообщение** внутри Telegram.

## Статьи на telegra.ph

Длинный текст в Telegram лучше живёт статьёй: ссылка на telegra.ph открывается
через Instant View прямо в приложении. tgx делает статью из маркдауна:

```bash
tgx article account --name tgx --author "Alex"        # один раз: аккаунт и токен
tgx article new "Заголовок" --file post.md --publish @mychannel
tgx article new "Заголовок" --file post.md --publish @mychannel --as @my_bot
tgx article edit Zagolovok-08-28 "Новый заголовок" --file post.md
tgx article list
```

Поддерживается ровно то, что Telegraph умеет: `#` → h3, `##` → h4, абзацы,
списки `-` и `1.`, цитаты `>`, блоки кода, разделители `---`, картинки, ссылки,
`**жирный**`, `*курсив*`, `` `код` ``, `~~зачёркнутый~~`. Токен telegra.ph даёт
право править все ваши статьи, поэтому лежит в `data/telegraph.json` с правами
`600` и показывается маской.

## Поиск с фильтрами

`ctrl+f` открывает поиск с полями: где искать (этот чат / все чаты), тип
сообщений, отправитель и границы дат. Тип можно задать и без текста запроса —
например «все ссылки за последнюю неделю».

| тип | что попадёт |
|---|---|
| `photo` · `video` · `media` | фото, видео, и то и другое |
| `file` · `link` · `voice` · `music` · `gif` · `round` | документы, ссылки, голосовые, музыка, гифки, кружки |
| `mention` · `pinned` | упоминания вас · закреплённые |
| `geo` · `contact` · `poll` | геометки, контакты, опросы |

Даты понимают `2026-08-01`, `01.08.2026` и относительное `-7d` / `-12h`.

```bash
tgx search "релиз" --peer @channel --since -30d
tgx search --kind link --peer @channel --since 2026-08-01 --until 2026-08-28
tgx search "договор" --kind file --global
tgx search "" --kind mention --global --limit 50
tgx search "спасибо" --peer @chat --sender @alex
```

Две честные границы, они идут от самого Telegram: у глобального поиска нет поля
отправителя, поэтому `--sender` работает только вместе с `--peer`; и нижнюю
границу дат сервер не принимает, поэтому `--since` применяется при чтении —
результаты идут от новых к старым, и выборка просто останавливается на границе.

## Темы форума и закрепления

В группе с включёнными темами над перепиской появляется полоса тем: переключение
переносит и чтение, и отправку в выбранный тред. У обычных чатов полоса скрыта.

- **`t`** — список тем: `enter` открыть, `n` создать (название берётся из поля
  внизу), `r` переименовать, `c` закрыть/открыть, `p` закрепить.
- **`shift+p`** — закрепить или открепить выделенное сообщение. Закрепление
  **тихое**: участников оно не будит. Закреплённые помечены `📌` прямо в бабле.

```bash
tgx topics @forum                       # список тем
tgx topic-create @forum "Релизы"
tgx topic-edit @forum 12 --title "Релизы 2026" --close
tgx topic-pin @forum 12                 # или --unpin
tgx channel-edit @group --forum 1       # включить темы в существующей группе

tgx pin @chat 4321                      # тихо; --notify разбудит участников
tgx pin @chat 4321 --unpin
tgx pinned @chat --limit 20
```

Агентам доступны `list_topics`, `list_pinned` на чтение и `create_topic`,
`edit_topic`, `pin_message` на запись — последний по умолчанию тоже тихий,
а `notify: true` в его описании отдельно помечен как то, о чём стоит спросить
пользователя.

## Боты

API для создания бота у Telegram нет — это разговор с @BotFather. tgx ведёт его
за вас и проверяет каждый шаг: если BotFather ответил не то, вы увидите **его
собственные слова**, а не «что-то пошло не так». Интерфейс разговорный и
формулировки в нём иногда меняются, поэтому это самая хрупкая часть — и самая
подробная по диагностике.

**А как же `bots.createBot` из слоя 227?** Он существует, но заменой BotFather не
является, и это стоило проверки. Вызов из своего аккаунта отвечает
`MANAGER_INVALID`: управляющим обязан быть бот. Подставляешь своего бота —
`MANAGER_PERMISSION_MISSING`: этому боту BotFather должен был выдать право
«управлять ботами». То есть путь работает для тех, кто строит ботов ботами, а не
для «завести бота из терминала». Он доступен как `--manager`, но по умолчанию
`bot create` идёт через BotFather — он единственный отдаёт токен владельцу.

Так же устроены `exportBotToken`, `getBotCommands`, `getBotMenuButton`: это
вызовы **для бота**, из пользовательского аккаунта приходит `USER_BOT_REQUIRED`.
Поэтому `bot commands`, `bot menu-get` и `bot group-rights` принимают имя бота и
работают под его сессией. А `bot mine` и `bot previews` спрашиваются от вашего
имени — и `mine` теперь мгновенный, без переписки с BotFather.

```bash
tgx bot mine                              # ваши боты, одним вызовом
tgx bot commands tgx_ops_bot              # команды глазами пользователя
tgx bot menu-get tgx_ops_bot              # какая сейчас кнопка-меню
tgx bot previews tgx_ops_bot              # картинки до запуска
tgx bot group-rights tgx_ops_bot --pin --delete
tgx bot create "Имя" myhelperbot --manager tgx_ops_bot   # если право выдано
```

```bash
tgx bot create "Мой бот" my_bot           # создать и сохранить токен
tgx bot list                              # токены показаны маской
tgx bot token @my_bot --reveal            # забрать токен у BotFather
tgx bot revoke @my_bot                    # отозвать и получить новый
tgx bot setname @my_bot "Новое имя"       # setabout / setdescription / setcommands
tgx bot info @my_bot                      # как бота видит API
tgx bot mine                              # каких ботов знает BotFather
tgx bot me @my_bot                        # вход по токену: кто это
tgx bot forget @my_bot                    # убрать из локального реестра
```

Имя, «о боте», описание и список команд ставятся **прямо через API**
(`bots.setBotInfo`, `bots.setBotCommands`) — там нечему сломаться. Через диалог с
BotFather идёт только то, для чего API не существует: создание бота и получение
или отзыв токена. Если всё же нужен старый путь — флаг `--via-botfather`.

Токены — учётные данные: `data/bots.json` пишется с правами `600`, в выводе
маскируется (`123456789:AAE…abcd`), целиком печатается только по `--reveal`
и агентам не отдаётся никогда.

### Посты от имени бота, кнопки и мини-приложения

Инлайн-кнопки под постом может вешать только бот — у личного аккаунта такой
возможности в Telegram нет:

```bash
tgx bot post @channel "**Релиз 1.0**" --as @my_bot \
  --button "Скачать=https://example.com/dl, Открыть=webapp:https://app.example.com ; Отзыв=cb:feedback"
```

| синтаксис | кнопка |
|---|---|
| `Текст=https://…` | ссылка |
| `Текст=webapp:https://…` | мини-приложение (Web App) |
| `Текст=cb:данные` | callback — отвечает работающий бот |
| `Текст=switch:запрос` | поделиться инлайн-запросом |
| `Текст=copy:значение` | копирование текста |
| `Текст=user:12345` | профиль пользователя |

Ряды разделяются `;`, кнопки в ряду — запятой; шпаргалка — `tgx bot buttons`.
Мини-приложение вешается и на меню самого бота:

```bash
tgx bot menu @my_bot --text "Открыть" --url https://app.example.com
tgx bot menu @my_bot --reset
```

В TUI то же самое в редакторе поста (`p`): выпадающий список «от себя / @бот» и
поле кнопок. Если выбрать кнопки от личного аккаунта, редактор предупредит и не
отправит — Telegram такое не примет.

Агентам доступны `list_bots` (только имена, без токенов) и `bot_post` — публикация
от имени бота со всеми типами кнопок.

## Правка текста руками Telegram

У сервера есть то, чего нет в Bot API: он вычитывает текст, расставляет эмодзи,
переводит и переписывает в заданном тоне. Возвращает не только результат, но и
**разметку правок** — что заменено, что убрано, что добавлено. Мы её
показываем: принимать чужую правку вслепую — плохая привычка.

```bash
tgx ai compose "превет колега завтро релиз"
tgx ai compose "релиз завтра, всё готово" --tone viking
tgx ai compose письмо.txt --translate en --apply @roman
```

Правки приходят как разметка поверх слитого текста, где куски старой и новой
версии лежат рядом. Мы проходим его один раз и собираем одну строку:

```
[-превет-]{+Превет,+} колега{+. Завтра+} [-завтро -]релиз{+.+}
```

Глазами это разбирается быстрее списка, поэтому строка идёт первой, а список —
под ключом `подробно`.

Тонов семь встроенных (`formal`, `short`, `tribal`, `corp`, `biblical`,
`viking`, `zen`), и можно завести свой — эмодзи, название и указание, как
писать. Свои тоны живут как наборы стикеров: их ставят, ими делятся, их удаляют.

```bash
tgx ai tones
tgx ai tone-example viking
tgx ai tone-new "Сухо" "Пиши коротко и без прилагательных" --credit
tgx ai tone-save чужой-тон
tgx ai tone-delete мой-тон --confirm-to @me      # исчезнет у всех, кто поставил
```

## Пересказ и перевод

Помимо правки, сервер умеет **пересказывать** и **переводить**. В графическом
клиенте это кнопки, о которых половина людей не знает; в терминале это команды.

```bash
tgx ai summarize durov 544 --lang ru      # длинный пост в три фразы
tgx ai translate en --text "Завтра релиз"
tgx ai translate ru --peer durov --id 544 # перевести чужое сообщение
tgx ai auto-translate чат on              # полоска «перевести» в чате
```

Отсюда получается то, ради чего терминал и нужен, — **сводка по чату**:

```bash
tgx ai digest durov --limit 20 --lang ru
```

Сервер пересказывает по одному сообщению, поэтому сводку `tgx` собирает сам:
длинное отдаёт на пересказ, короткое берёт как есть. Пересказывать «ага» смысла
нет, а лишний вызов стоит времени и попадает под ограничение частоты. Если
сервер на каком-то сообщении отказался, это видно строкой в ленте — сводка не
рушится целиком.

## Что ждёт внимания

В графическом Telegram непрочитанное видно боковым зрением: значки, кружки,
жирные точки. В терминале бокового зрения нет, поэтому нужны прямые вопросы.

```bash
tgx mentions чат              # где вас звали и вы ещё не видели
tgx my-reactions чат          # на что вам отреагировали
tgx triage-clear чат          # пометить и то и другое просмотренным
tgx read-by группа 42         # кто прочитал это сообщение
tgx read-at @roman 42         # когда прочитали ваше
tgx views канал 544 543       # просмотры и пересылки постов
tgx chat-counts чат           # чем набит: фото, видео, ссылки, файлы
tgx online чат                # сколько человек сейчас
tgx mark-unread чат           # вернуть жирную точку, чтобы не забыть
tgx pin-chat чат              # закрепить чат в списке
tgx no-forwards чат on        # запретить пересылку и копирование
```

`read-at` работает взаимно: пока вы прячете своё время прочтения, чужое вам не
покажут. Мы так и пишем в ошибке — это не поломка, а устройство.

## Уведомления, заявки и реакции

Три темы, которые в графическом клиенте лежат по разным углам, а на деле про
одно: кто может вас разбудить и кого вы впускаете.

```bash
tgx notify show                       # как настроено для личных чатов
tgx notify mute 2h "AllCrew Cockpit"  # заглушить на два часа
tgx notify mute forever чат           # насовсем
tgx notify mute off чат               # снять
tgx notify mute 8h --scope channels   # все каналы разом
tgx notify reactions --from contacts  # реакциями будят только контакты
tgx notify new-contacts off           # молчать о регистрации знакомых
```

Тишина у Telegram считается не «часами молчания», а **отметкой времени, до
которой чат молчит**. Отсюда две ловушки, обе видны в выводе, если их не
обработать:

* незаглушённый чат приходит нулём, и Telethon превращает ноль в 1 января 1970
  года — «тихо до 1970 года» было бы враньём, поэтому и ноль, и любую дату в
  прошлом мы считаем отсутствием тишины;
* «навсегда» — это дата на краю тридцатидвухбитного времени, а не отдельный
  признак; мы её так и называем словом.

Заявки на вступление — оборотная сторона именных приглашений:

```bash
tgx notify requests чат                    # кто из админов сколько ссылок наделал
tgx notify approve чат @roman              # принять
tgx notify decline чат @stranger           # отклонить
tgx notify decline-all чат --link https://t.me/+…   # всех по одной ссылке
tgx notify invite-edit чат ССЫЛКА --limit 1 --expires 2h --request-needed
tgx notify invite-purge чат                # выбросить отозванные — они копятся
```

`invite-edit` правит выпущенную ссылку, не выпуская новую: это важно, когда
ссылка уже кому-то отправлена, а условия поменялись.

Реакции:

```bash
tgx notify emoji-list                 # какие вообще есть
tgx notify emoji-top --limit 8        # какие ставят чаще всего
tgx notify emoji-default 🔥           # ваша по умолчанию
tgx notify emoji-allow чат 👍 🔥 ❤    # что можно в этом чате
tgx notify emoji-allow чат all        # любые
tgx notify emoji-allow чат            # запретить все
```

## Стикеры: не только собирать, но и пользоваться

Собрать набор `tgx` умел давно, а вот найти и отправить стикер — нет.

```bash
tgx stickers installed              # какие наборы у вас стоят
tgx stickers mine                   # какие вы сделали сами
tgx stickers find-sets "cat"        # найти набор
tgx stickers find --emoji 🔥        # найти отдельные стикеры
tgx stickers recent                 # недавние
tgx stickers faved                  # избранные
tgx stickers install LadyCat        # поставить набор
tgx stickers send @roman 4909…:-629…
```

Две вещи, которые выясняются только на практике:

* **Отправить стикер файлом нельзя.** Загруженный заново `.webp` станет обычной
  картинкой: стикером его делает принадлежность набору, а её несёт только
  исходный документ. Поэтому `find` печатает ключ вида `id:hash` — это то, чем
  ссылаются на существующий стикер, и без второй половины сервер отвечает, что
  документа не существует.
* **Поиск по эмодзи возвращает не то, что вы искали.** Сервер подбирает стикеры
  по связям, и собственное эмодзи стикера обычно другое: спросишь 🔥, получишь
  😡. Поэтому поле называется «своё эмодзи», а не «эмодзи» — чтобы не выглядело
  ошибкой.

Часть наборов приходит без короткого имени — у них его попросту нет. Сослаться
на такой можно только числами, поэтому `set_ref` принимает и `id:hash`.

## Видео в богатом сообщении

Через разметку не выйдет: ссылка `tg://photo?id=` принимает только фотографию,
а `tg://video?id=` не существует — сервер отвечает `RICH_MESSAGE_PHOTO_INVALID`.
Раньше туда молча уезжал mp4 и превращался в стоп-кадр.

Работает блочная форма. Проверено на живом сервере: приходит `PageBlockVideo` с
настоящим mp4 внутри.

```json
[
  {"type": "heading", "size": 1, "text": "Заголовок"},
  {"type": "video", "video": {"type": "video", "media": "attach://banner"}},
  {"type": "paragraph", "text": "Подпись рядом"}
]
```

```bash
tgx bot rich чат --as мойбот --blocks blocks.json --attach banner=баннер.mp4
```

Ссылка `attach://` лежит в поле, **названном по типу блока**: у документа в
`document`, у видео в `video`, у фотографии в `photo`. Пока `tgx` искал её
только в `document`, у видеоблока имя выходило пустым — и в форму уезжало поле
без имени. Сервер на такое отвечает `400` с **пустым телом**, а пустое тело
превращалось в `Expecting value: line 1 column 1`. Теперь пустой ответ называется
пустым ответом, а имя поля берётся по типу блока.

Собирать дерево руками не обязательно: `--as-blocks` переводит вашу разметку в
блоки у себя, а не на сервере, — и тогда `![](tg://video?id=имя)` работает.

```bash
tgx bot rich чат --as мойбот --file пост.md --as-blocks --attach banner=баннер.mp4
```

Форма текста внутри блока выяснена опытом, в документации её нет:

| что пишете | что получается |
| --- | --- |
| `"строка"` | `TextPlain` |
| `["часть ", {…}]` | `TextConcat` |
| `{"type": "bold", "text": …}` | `TextBold`; так же italic, code |
| `{"type": "url", "text": …, "url": …}` | `TextUrl` |

Узлы вкладываются друг в друга. Привычная по обычным сообщениям пара
`{"text": …, "entities": […]}` здесь **не принимается вовсе** — сервер отвечает
`Can't find field "type"`.

Помните про ограничение блочной формы: она новее слоя MTProto, который знает
Telethon, поэтому отправить её можно, а прочесть обратно в `tgx` — только
структуру, не содержимое документа.

## Мини-приложение, запущенное из терминала

Подписанный адрес, открытый в обычной вкладке, даёт **сломанное** приложение.
Мини-приложение ждёт вокруг себя хозяина: скрипт внутри страницы шлёт наружу
«я готов», «дай тему», «покажи главную кнопку», «закрой меня» — и ждёт ответов.
В браузере отвечать некому, поэтому приложение либо висит на заставке, либо
считает, что запущено вне Telegram.

```bash
tgx inline run wallet          # своё окно вокруг приложения
tgx inline run мойбот --param ref_12345
```

`tgx` изображает хозяина: локальная страница держит приложение в рамке,
разговаривает с ним по тому же протоколу и рисует его кнопки своими руками.
Главную кнопку и кнопку «назад» видно в окне; данные, которые приложение
отправляет, печатаются в терминал.

Чего мы не изображаем: оплату, доступ к контактам, камеру, отпечаток пальца.
Такие запросы получают **отказ вслух**, и он виден в окне. Честный отказ лучше
тишины, в которой непонятно, что сломалось.

**Приложение подаётся с нашего адреса.** Иначе никак: чужая страница может
запретить себя встраивать, а её содержимое в любом случае закрыто правилом
одного происхождения — ни прочитать, ни нажать. Проводник превращает любой адрес
в путь `/x/<схема>/<хост>/<путь>`, и приложение оказывается обычной страницей
внутри нашей.

Что при этом пришлось выяснить на живых приложениях, по одной поломке за раз:

* **Код переписывать нельзя.** В нём адрес текстом не отличить от чего угодно
  другого: `/https?:\/\//gi` — это регулярное выражение, и подмена внутри него
  ломает разбор всего файла. Приложение падало на «invalid regular expression
  flags», пока я правил код наравне с разметкой. Теперь за код отвечает перехват
  во время работы — он видит настоящие адреса, а не похожий на них текст.
* **Якорь обязателен.** Telegram кладёт в него подписанные данные запуска, а на
  сервер якорь не уходит вовсе. Отбросив его, мы отдавали приложению адрес без
  пропуска, и оно останавливалось на «Telegram initData is missing».
* **Пути от корня ведут к нам, а не к приложению.** Вместо скриптов ему
  отдавалась наша же разметка, и оно молча не запускалось: браузер ругался на
  «MIME type text/html» для модулей.
* **HTTP/1.0 не годится.** Браузер отказывался подгружать куски приложения при
  том, что тот же адрес прекрасно отдавался `curl`.
* **Чужой заголовок доступа отвергает наши куски** — сервер разрешал доступ
  своему домену, а мы теперь под другим.
* **Происхождение надо помнить только от страницы.** Раньше его перезаписывал
  каждый ответ, включая чужие домены со шрифтами, и запасной путь уводил запросы
  к ним. Ответы приходят из разных потоков, поэтому промах был не всегда, а
  через раз: то файл, то 404.
* **Приложение надо подавать по его собственному пути.** Почти всякое из них
  одностраничное, и маршрутизатор разбирает `location.pathname` сам. Под
  префиксом `/x/https/хост/bot/…` он видит путь, не начинающийся с ожидаемого
  `/bot`, и не рисует **ничего** — белый экран без единой ошибки. Префикс
  остался только для чужих доменов.
* **Происхождение надо закрепить за приложением.** Виджет поддержки внутри него
  тоже отдаёт HTML, и после его загрузки запросы приложения уходили к нему:
  сервер отвечал «wrong verify token», приложение вставало белым экраном.
* **Приложение может склеить свой адрес с нашим хостом.** Wallet строит адрес
  API, приклеивая поддомен к текущему хосту: дома выходит
  `alectryon.walletbot.me`, а у нас — `alectryon.127.0.0.1:51805`. Такое браузер
  не разбирает **вовсе**: `fetch` падает до сети, приложение уходит в
  `init_failed` и показывает «Some technical issue». Возвращаем службе её
  настоящий дом до разбора адреса.
* **Сообщение может быть адресовано `web.telegram.org`.** Часть приложений не
  грузит `telegram-web-app.js` вовсе, а шлёт события на конкретный адрес. Мы не
  он — браузер такое молча выбрасывает, приложение не дожидается ответа и через
  200 мс пишет «Environment Error · Please open this application directly
  through Telegram». Своё окно вправе принимать всё, что ему шлют, поэтому
  чужой адрес назначения доставке не мешает; в журнале это видно строкой.
* **Событиям, которые ждут ответа, надо отвечать.** Полноэкранный режим,
  «добавить на домашний экран», свои методы бота — молчание вешает приложение
  на заставке. «Тюряга» так и висела, пока не ответили на запрос полного экрана.
* **За переадресацией должен идти браузер, а не мы.** Библиотека следует за
  `3xx` молча, и страница возвращается по исходному пути: `esim-bot.mobile.ag/`
  уводит на `bot.mobile.tg/bot/`, а рамка остаётся на `/` — маршрутизатор
  приложения видит не тот путь и не рисует ничего. Теперь переадресацию
  отдаём браузеру, а первая же из них закрепляет настоящее происхождение.
* **Страница окна не может жить в корне.** У многих приложений путь — просто
  `/`, и окно грузило в рамку само себя: шапка рисовалась дважды, а приложение
  не появлялось вовсе. Окно ушло на `/__tgx/`, корень отдан приложению.
* **Вебсокеты надо вернуть на настоящий сервер.** Их адрес уже переписан на
  нас, а вебсокетов мы не подаём — приложение на ActionCable просто не
  запускалось.
* **`iframe_ready` требует ответа стилем.** Скрипт Telegram здоровается им ещё
  до самого приложения, и без ответа его подготовка не заканчивается —
  приложение падает по своему таймауту, с виду беспричинно.

**Что проверено, до конца загрузки.** Восемь приложений, каждое своим входом:

| приложение | что показывает |
| --- | --- |
| Wallet | баланс, Transfer / Deposit / Withdraw / Exchange |
| Antarctic Wallet | вкладки, предложение поставить PIN |
| TonMobile eSIM | тарифы, баланс, страны — после починки переадресации |
| Epic Gift | баланс, Rocket, PVP, Play Hub |
| Gorilla Case | кейсы, розыгрыши, задания, турнир |
| Random Games | «Earn Stars», разделы, витрина |
| Telegram Simulator | рулетка, апгрейд, вкладки |
| Тюряга | запускается за полторы минуты: 6 МБ кода и 70 файлов ресурсов |

Если приложение всё же откажется, окно скажет об этом словами и различит два
случая: «загрузилось и отказалось работать» и «не пустило нас в рамку» — по
тому, доступно ли нам её содержимое.

Отдельно — приложение может вовсе запретить себя встраивать; тогда содержимое
нам недоступно, и в окне это написано другими словами. Различаем по тому, видим
ли мы страницу внутри.

## Кнопки, открывающие приложение

**Обычным нажатием они не работают.** У Telethon в `click()` нет ветки для
`KeyboardButtonWebView`: нажатие молча возвращает пустоту, и `tgx` бодро
отвечал `"ok": true, "click_result": null`, ничего при этом не сделав. Со
стороны — «жму кнопку, ничего не открывается».

```bash
tgx inline press-app чат 295606 --text "Open Wallet" --run
tgx inline menu-button "Antarctic Wallet" --run
```

`press-app` берёт из кнопки её адрес и просит у сервера подписанный — тот
самый, который открыл бы настоящий клиент. Обычную кнопку он отвергает и
отсылает к `message-click`, а ненайденную — перечисляет, какие есть.

**Кнопка-меню — отдельная сущность**: не кнопка под сообщением, а свойство
самого бота, та, что слева от поля ввода. Её адрес лежит в **полном профиле
бота**, а не в `bots.getBotMenuButton`: тот отвечает только самому боту, а нам
нужна та кнопка, которую видит человек. Настоящий клиент берёт её оттуда же.

## Истории в полноэкранном клиенте

```
ctrl+o   лента историй
```

Слева список — автор, подпись, счётчик просмотров; справа сама картинка, тем же
способом, что и превью в переписке: Kitty, iTerm2, sixel, а где ничего нет —
юникодом.

```
j/k   листать          o   открыть целиком в системе
h     скрытые          s   скрытный просмотр
r     обновить         escape закрыть
```

**У видео показываем кадр, а не качаем ролик.** Терминал видео не играет, и
тянуть мегабайты ради превью незачем — берём то, которое Telegram и так отдаёт.
А `o` скачивает историю целиком и отдаёт системному просмотрщику: тем же путём,
что и вложения в переписке.

**Скрытный просмотр** (`s`) — возможность Telegram Premium: просмотры не
засчитываются ни за прошедшие минуты, ни за ближайшие. Без Premium сервер
откажет, и отказ показывается как есть.

Автор в ленте приходит числом, имена спрашиваются по одному разу на каждого: у
одного человека в ленте обычно несколько историй подряд.

## Все команды в полноэкранном клиенте

Долгое время окно и командная строка расходились: команд у `tgx` под шесть
сотен, а в окне их было два десятка — те, что повешены на клавиши. Остальное из
окна было недоступно вовсе.

```
ctrl+p   палитра команд — поиск по названию и описанию
ctrl+w   мини-приложение бота из его чата
```

Список в палитре **не написан руками**, а выведен из дерева разбора командной
строки — оттуда же, откуда его берёт коннектор для агентов. Новая команда
появляется в окне сама. Это не экономия труда: два списка, которые надо помнить
обновлять, однажды разойдутся, и заметить это можно будет только по жалобе.

Поля ввода палитра тоже берёт из разборщика: обязательные сверху,
необязательные ниже, выключатели галочками, а где у аргумента есть список
допустимых значений — показывается именно он.

Соединение при этом одно. Команда, открывшая своё, упёрлась бы в занятый файл
сессии — тот самый «database is locked», — поэтому ей одалживают уже открытый
клиент окна, а закрытие делают пустым действием. Тот же приём, что и в
коннекторе.

`ctrl+w` вынесен отдельно, потому что кнопка-меню — **свойство самого бота**, а
не сообщения: в графическом клиенте она слева от поля ввода, и из чата с ботом
её жмут чаще всего.

Числа палитра приводит сама. Поля ввода отдают строки — иначе им неоткуда
взяться, — а командная строка прогоняет их через `type=int` при разборе,
которого мы не делаем. Без приведения ломается не в палитре, а глубоко внутри:
то `'>' not supported between int and str`, то `struct.error: required argument
is not an integer`. По такому сообщению до причины не добраться, поэтому
приведение стоит до вызова, а негодное значение называет своё поле.

Ещё одна мелочь, стоившая отладки: `ctrl+p` занят встроенной палитрой Textual.
Пока обе были включены, выигрывала её — наша просто не открывалась, и по
нажатию понять это было нельзя.

## Представиться телефоном

Многие мини-приложения рисуют себя по-разному на телефоне и на настольном
клиенте, а иные показывают на настольном заглушку.

```bash
tgx inline run бот --as android --phone
```

`--as` меняет платформу в подписанном адресе, `--phone` подменяет само
устройство: касания и угол экрана. Настоящий телефон в альбомной ориентации
сообщает угол 90, настольный — ноль всегда, и по этому признаку приложения
отличают одно от другого. Подменять надо **до** загрузки: приложение измеряет
себя первым делом, после загрузки уже поздно.

Это то же, что выбор устройства в отладчике браузера, и по умолчанию всё
выключено.

**Про «Тюрягу» я сперва написал неверно, и это стоит запомнить как урок.**
Мне показалось, что игра просит альбомную ориентацию и не запускается. На деле
её блок `#rotate-overlay` лежит в разметке всегда и по умолчанию скрыт:

```js
const надо = (платформа === 'android' || платформа === 'ios' ||
              matchMedia('(pointer:coarse)').matches)
             && (innerWidth < innerHeight && innerWidth / innerHeight < 0.75);
```

Оба условия у нас ложны, и блок не показывался ни разу. Я читал его текст
**сырым `textContent`, который берёт и скрытое**, — и пять раз подряд искал
причину там, где её не было. Собственный инструмент `page` при этом отвечал
правильно: он пропускает `display: none` и возвращал пустоту, потому что игра
рисует себя на холсте.

А стояла игра просто потому, что грузилась: 6 МБ кода и семьдесят файлов
ресурсов. Через полторы минуты запустилась сама.

Переключатель платформы всё равно полезен — Gorilla Case в режиме телефона
показывает подсказку, которой на десктопе нет, — но «Тюрягу» он не чинил и
чинить было нечего.

## Навигация в окне приложения

У окна своя шапка: назад, вперёд, обновить, в начало — и адрес, на котором вы
сейчас внутри приложения. Без них из приложения некуда деться: кнопку «назад»
рисует оно само и далеко не всегда, а обычная история браузера внутри есть
всегда.

```bash
tgx window do back      # назад по истории приложения
tgx window do forward
tgx window do reload
tgx window do home      # к экрану, с которого начали
```

## Агент внутри приложения

Приложение подано с нашего адреса — значит, оно видно и нажимаемо целиком, со
своей навигацией, списками и полями.

```bash
tgx window do page                        # весь видимый текст приложения
tgx window do elements                    # что можно нажать и заполнить
tgx window do click --args '{"ref":"e1"}'
tgx window do fill --args '{"ref":"e2","text":"привет"}'
tgx window do scroll --args '{"y":600}'
tgx window do go --args '{"url":"/settings"}'
```

Проверено на настоящем чужом приложении: агент прочитал его текст, нашёл кнопку
`Show Error`, нажал её и прочитал то, что приложение показало в ответ.

Поле заполняется через нативный сеттер значения, а не присваиванием: иначе React
и подобные изменения не замечают.

`page` и `layout` берут текст из `textContent`, а не `innerText`, а видимость
проверяют обходом предков по вычисляемому стилю. Причина одна: `innerText`,
`offsetParent` и координаты отражают **отрисовку**, и у фоновой вкладки они
пусты, а `textContent` и стиль живут в дереве и есть всегда. При этом сам
браузер усыпляет вкладку, когда она уходит в фон, — тогда останавливается и опрос
поручений. Поэтому окно, с которым работает агент, должно быть активной вкладкой;
это его нормальное состояние — агент его открыл и с ним работает.

## Окна, которыми пользуется агент

Человек окно видит и жмёт кнопки. Агент — нет: окно живёт в браузере,
MCP-сервер в другом процессе, и общего между ними ничего. Здесь общее
появляется — по образцу [WebMCP](https://webmcp.dev/), где страница объявляет
свои действия сама.

```bash
tgx window list                   # какие окна открыты
tgx window read                   # что в окне и что в нём можно сделать
tgx window do press_main          # нажать главную кнопку приложения
tgx window do send_event --args '{"type":"theme_changed","data":{…}}'
```

Устройство трёхслойное, и каждый слой нужен по своей причине:

* **Реестр на диске.** Окно поднимает одна команда, а спрашивает про него другой
  процесс — без записи он не узнает даже адреса. Записи чистятся по живости
  процесса. Тут легко ошибиться: отказ в праве послать сигнал означает, что
  процесс есть, но чужой, — это «жив», а не «мёртв», и спутав, реестр вычистит
  рабочее окно.
* **Состояние, которое шлёт страница.** Что происходит внутри мини-приложения,
  знает только браузер: приложение чужое, его пиксели нам не видны. Зато оно
  разговаривает с нашей страницей, и всё сказанное страница пересылает нам —
  надпись на главной кнопке, есть ли «назад», что приложение прислало.
* **Поручения.** Агент просит нажать кнопку, но нажимает её страница. Поручение
  кладётся в очередь, страница забирает его, выполняет и приносит **результат**.
  Ждём именно результата: «нажал» без него — отчёт о намерении, по которому
  нельзя судить, что вышло.

Поручения окно ждёт **на сервере**, а не опросом по таймеру. Браузер душит
таймеры в фоновой вкладке — вплоть до одного срабатывания в минуту, — и окно
отвечало агенту пустотой просто потому, что на него никто не смотрел. Запрос
держится открытым до появления поручения; незавершённый запрос браузер так не
тормозит.

Сервер окна обязан быть многопоточным, и это не мелочь: поручение агента ждёт
ответа страницы, а страница за ответом ходит на тот же сервер. В один поток они
заперли бы друг друга насмерть.

Действия объявляет сама страница — встроенные `snapshot`, `press_main`,
`press_back`, `send_event`, `close` у мини-приложения и `snapshot`, `refresh` у
окна звонка. Своя страница может объявить свои:

```javascript
window.tgx.registerTool('имя', 'что делает', {параметр: {type: 'string'}},
                        (args) => ({вышло: true}));
```

Наружу окна не смотрят: адрес обязан быть на `127.0.0.1`, иначе `tgx` откажется
с ним разговаривать.

## Кнопка-меню бота

Видов у неё три, а не два, и средний легко пропустить:

```bash
tgx bot menu мойбот --url https://app.example --text "Открыть"
tgx bot menu мойбот --commands            # показывать список команд
tgx bot menu мойбот --reset               # как по умолчанию
tgx bot menu мойбот --url … --for-user @roman   # своя кнопка одному человеку
```

`--commands` — ровно то, чего хотят от бота без приложения. А `--for-user`
делает персональную кнопку: у одного человека своя, у остальных общая, — так
вешают панель администратора.

## Стикеры: библиотека

```bash
tgx stickers featured             # что Telegram рекомендует
tgx stickers archived             # убранные в архив: не видны, но не удалены
tgx stickers emoji-groups         # разделы панели эмодзи
tgx stickers archive Набор        # убрать; --restore вернуть; --remove снести
tgx stickers gifs
tgx stickers whose чат 42         # из какого набора стикеры на фотографии
```

## Чужие боты из терминала

Инлайн-бот — это тот, кого вызывают через `@бот запрос` прямо в поле ввода:
@gif, @pic, @vid и тысячи других. Обычно результаты выбирают пальцем из
всплывающего списка; здесь список печатается, а выбор делается номером.

```bash
tgx inline ask gif "cat"                    # спросить, получить метку и список
tgx inline send me 2059… gif5797…           # отправить выбранное
tgx inline start @somebot ref_12345         # «Начать» с параметром из ссылки
tgx inline press чат 4821 "vote_yes"        # нажать кнопку и услышать ответ
```

Метка списка живёт недолго. Когда сервер отвечает `INLINE_RESULT_EXPIRED`, это
не поломка, а срок годности — надо спросить заново; мы так и пишем.

`press` умеет кнопки, которые требуют пароль двухфакторной защиты, — так сделаны
кнопки, подтверждающие траты. Пароль спрашивается у терминала, а не берётся из
аргументов, чтобы не осесть в истории оболочки.

Мини-приложения:

```bash
tgx inline attach-list                # боты в меню вложений и где они работают
tgx inline attach @bot on --allow-write
tgx inline web-app wallet --open      # подписанный адрес, сразу в браузере
```

У Telegram **три разных вызова** под три способа открыть одно и то же
приложение: из бокового меню, из чата и по короткому имени. Какой подойдёт,
зависит от того, как бот его завёл, и снаружи это неизвестно. Поэтому `tgx`
пробует боковое меню, а на отказ переходит к чату: для человека это одно
действие, а не выбор из трёх непонятных.

Полученный адрес **подписан вашей сессией** — по сути это ключ от аккаунта
внутри приложения. Он не пишется в файлы и никуда не уходит, кроме браузера;
предупреждение печатается рядом с адресом каждый раз.

## Блокировки, жалобы, уборка

То, что делают, когда всё пошло не так. Почти всё здесь необратимо, поэтому
идёт через то же подтверждение с кнопками, что и платежи.

```bash
tgx safety blocked                          # кого вы заблокировали
tgx safety block @stranger --confirm-to @me
tgx safety block @author --stories-only     # закрыть только истории
tgx safety unblock @stranger
tgx safety peer-settings @кто-то            # что Telegram думает о собеседнике
tgx safety clear-history @кто-то --both-sides --confirm-to @me
tgx safety unpin-all чат --confirm-to @me
tgx safety sponsored off                    # скрыть рекламу (Premium)
```

`peer-settings` — это не украшение: полоска «заблокировать / пожаловаться» над
новым чатом собирается из ответа сервера, и его можно спросить прямо. Там же
видно, берёт ли человек звёзды за сообщения и из какой страны его номер.

`block-replier` существует отдельно потому, что в комментариях под каналом вы
часто не знаете, кто это, — знаете только сообщение:

```bash
tgx safety block-replier 4821 --delete --spam --confirm-to @me
```

**Жалоба идёт по меню сервера, а не по списку в коде.** Причины у Telegram
меняются, и зашивать их к себе — значит устареть к следующему обновлению.
Поэтому первый вызов показывает то, что прислал сервер, а второй передаёт
выбранное обратно. Варианты приходят байтами, для терминала мы кодируем их в
base64 и разворачиваем ровно в те же байты:

```bash
tgx safety report чат 4821
# → шаг: «За что?»; варианты: Spam (ключ OQ), Copyright (ключ OA), …
tgx safety report чат 4821 --option OQ --confirm-to @me
```

## Остаток по сообщениям

```bash
tgx msgx vote чат 42 2            # проголосовать за второй вариант
tgx msgx voters чат 42           # кто как проголосовал
tgx msgx link-preview "https://…"   # что покажется под ссылкой, не отправляя
tgx msgx chat-theme @roman 🎄     # тема отдельного чата
tgx msgx who-reacted чат 42 43    # сводка реакций
tgx msgx saved-dialogs           # под-чаты избранного по автору
tgx msgx fact-check чат 42 "проверено" --confirm-to @me
```

Голоса задаются номерами, а сервер ждёт байты вариантов — их нельзя угадать,
`tgx` берёт их из самого опроса. Иначе легко проголосовать не за то.

Ещё оттуда же — реклама, вход по кнопке, календарь вложений, приглашения:

```bash
tgx msgx sponsored канал          # какая реклама показывается
tgx msgx url-auth чат 42 0        # что предлагает «войти через Telegram»
tgx msgx search-calendar чат --kind photo   # по каким дням есть фото
tgx msgx check-invite "https://t.me/+…"     # что за чат, не вступая
tgx msgx scheduled чат 5 6        # отложенные по номерам
tgx msgx personal-channel @кто-то # посты его личного канала
```

Ключ рекламы — байты; в командную строку он идёт шестнадцатеричным и
разворачивается обратно при отметке просмотра. `url-auth` показывает, что
именно сайт узнает о вас, **до** согласия — потому и разведён на два шага.

## Перенос переписки и прочий остаток

```bash
tgx msgx import-target чат          # годится ли чат для переноса
tgx msgx import-start чат выгрузка.txt
tgx msgx import-media чат 42 фото.jpg
tgx msgx import-run чат 42
tgx msgx web-page "https://…"       # мгновенный просмотр, если он есть
tgx msgx future-owner чат           # кому достанется чат, если выйти
tgx msgx suggested-folders          # какие папки Telegram предлагает
```

Перенос устроен в четыре шага, и порядок неслучаен: сервер сперва узнаёт формат
по первым строкам, потом проверяет чат, потом заводит перенос, и лишь затем
доливаются вложения и запускается сборка. Пропустить шаг нельзя — откажет, но
объяснит скупо.

Мелочь, стоившая правки: название предлагаемой папки лежит **внутри самой
папки**, а не на обёртке с описанием. Читая с обёртки, получаешь `null` там, где
на самом деле «Unread».

## Настройки и оформление аккаунта

Большая часть здесь в терминале не рисуется — обои, темы, наборы
эмодзи-статусов, — но их можно ставить, и это меняется на всех ваших клиентах.

```bash
tgx account sensitive on            # показывать деликатный контент
tgx account username второйадрес off   # выключить дополнительный адрес
tgx account main-tab gifts          # что показывать первым в профиле
tgx account wallpapers              # какие обои доступны
tgx account themes --chat           # готовые темы для чатов
tgx account autosave                # что скачивается само
tgx account set-autosave users --photos --videos --max-mb 50
tgx account paid-revenue @кто-то    # сколько принесли платные сообщения
```

Тонкое место — дополнительные адреса. У аккаунта их бывает несколько, включённые
показываются в профиле ссылками. Список один, а управляют по имени: включить,
выключить, переставить. Перепутать основной и дополнительный легко — тогда
профиль остаётся без адреса вовсе.

## Выгрузка аккаунта

`tgx takeout` — та самая «экспорт данных» из настроек, только из терминала.
Telegram даёт под неё отдельный режим сессии: внутри него ограничения на частоту
мягче, поэтому историю можно выкачивать сотнями сообщений. Пишем на диск по ходу
дела: у людей бывают чаты, которые в память не влезают.

```bash
tgx takeout ~/выгрузка                                   # контакты и список чатов
tgx takeout ~/выгрузка --chat "AllCrew Cockpit" --files   # и история со вложениями
tgx takeout-finish                                        # закрыть висящую
```

Две ловушки, обе стоили отладки:

1. **Пауза — это защита, а не поломка.** На первый запрос Telegram может
   ответить `TAKEOUT_INIT_DELAY` и прислать в другое ваше устройство
   подтверждение. Смысл в том, что если аккаунт угнали, у вас есть время
   заметить. Мы так и пишем в ошибке, вместо «ошибка 420».
2. **Пустой признак выгрузки в сессии.** Хранилище кладёт `takeout_id` в
   поле-блоб, и незаполненное поле читается как `b''`, а не `None`. Telethon
   сравнивает с `None` — и решает, что выгрузка уже идёт; сервер же на `b''`
   отвечает ошибкой упаковки, ещё до сети. Получается выгрузка, которую нельзя
   ни начать, ни закончить. `tgx` приводит пустое значение к `None` перед
   работой, а `takeout-finish` честно говорит, было ли что закрывать.

## Обычные группы и «печатает…»

Обычная группа — не супергруппа: без истории для новичков, без адреса, без
ролей сложнее «администратор». Telegram её не убрал, и часть переписки у людей
живёт именно в таких.

```bash
tgx group new "Ремонт" @roman @kate     # в одиночку нельзя, Telegram требует людей
tgx group add чат @kate --history 100   # и показать ей сто прошлых сообщений
tgx group admin чат @roman              # права
tgx group rank чат @roman "техдиректор" # звание вместо слова «админ»
tgx group upgrade чат --confirm-to @me  # в супергруппу; обратно нельзя
tgx group hand-over чат @roman --confirm-to @me
```

Обычная группа адресуется **голым числом** — единственное место в схеме, где
так: у канала `InputChannel`, у человека `InputUser`, а тут просто `chat_id`.
Перепутать легко, и сервер отвечает невнятным `CHAT_ID_INVALID`, поэтому `tgx`
проверяет сам и отсылает к командам `channel-*`.

Признак набора в графическом клиенте ставится сам, в терминале — нет: терминал
не знает, что вы набираете именно сообщение. Собеседник всё это время видит
тишину.

```bash
tgx group typing @roman              # «печатает…»
tgx group typing @roman voice        # «записывает голосовое…»
tgx group typing @roman file --progress 60
tgx group typing @roman cancel       # передумали
```

## Ветки обсуждений

Пост живёт в канале, а комментарии — в связанной группе, и там у них свой
номер. Без этого перехода отвечать в ветку не по чему.

```bash
tgx group thread канал 6992     # → в обсуждении id 6464, ответов 2
tgx group replies канал 6992
tgx group read-thread канал 6992
```

## Своё имя важнее чужого

Название вашего чата может совпасть с чужим адресом. У меня так вышло с
`PLOMBIR`: это и мой канал, и посторонний `@Plombir`, — и глобальный поиск
отдавал постороннего. Написать не туда так проще простого.

Теперь точное совпадение с названием своего диалога решает спор. Обратный
порядок для `@имени` и адреса в нижнем регистре: их пишут, когда имеют в виду
именно адрес, — `tgx send durov` по-прежнему идёт к `@durov`, а не ищет чат с
таким названием.

## Каналы: остаток управления

То, что в графическом клиенте разбросано по подменю настроек.

```bash
tgx chan search-posts "telegram cli"   # поиск по постам всего Telegram
tgx chan search-quota                  # сколько поисков осталось бесплатно
tgx chan username канал второйадрес off
tgx chan autotranslate канал on        # кнопка перевода у читателей
tgx chan main-tab канал gifts          # что показывать первым в профиле
tgx chan stickers группа НаборИмя      # общий набор для участников
tgx chan paid-messages группа 5        # звёзд за сообщение
tgx chan author канал 6992             # кто из админов написал пост
tgx chan left                          # каналы, из которых вы вышли
tgx chan delete канал --confirm-to @me
```

Две ловушки, обе в ответах сервера:

* **`GetLeftChannels` работает только внутри выгрузки.** Сервер считает этот
  список историей, а не текущим состоянием, и снаружи отвечает
  `TAKEOUT_REQUIRED`. `tgx chan left` открывает выгрузку сам и закрывает за
  собой: просить об этом человека странно, он спросил всего лишь список.
* **`send-as` отвечает `PEER_ID_INVALID` и когда чата нет, и когда писать от
  чужого имени тут просто нельзя.** Выбор появляется у админов канала с
  привязанной группой и у анонимных админов; в обычной группе его не бывает. Мы
  так и пишем — иначе выглядит как «чат не найден».

## Витрина бота и его настройки

```bash
tgx bot previews-info мойбот      # что человек видит до «Начать»
tgx bot preview-add мойбот https://…/demo.mp4
tgx bot popular                   # какие мини-приложения сейчас смотрят
tgx bot similar мойбот
tgx bot referrals мойбот 150      # партнёрам 15%
tgx bot can-write мойбот
tgx bot free-name новыйбот
```

Доля партнёра считается **в тысячных**, а не в процентах: `150` — это 15%.
Легко перепутать и отдать вдесятеро больше, поэтому `tgx` печатает результат
процентами.

Превью принимают только публичный адрес — локальный файл сначала надо куда-то
выложить. Ссылаться на превью можно лишь тем же медиа, каким его положили:
отдельного идентификатора у них нет.

## Что отдают только внутри выгрузки

Два списка сервер считает персональными данными, а не текущим состоянием, и
снаружи отвечает `TAKEOUT_REQUIRED`: покинутые каналы и то, что он запомнил из
вашей телефонной книги. Просить человека открыть режим экспорта ради списка
странно, поэтому `tgx` открывает выгрузку сам и закрывает за собой.

```bash
tgx chan left        # каналы, из которых вы вышли
tgx contacts saved   # что Telegram помнит из вашей телефонной книги
tgx contacts forget-saved   # заставить забыть
```

## Контакты: остаток

```bash
tgx contacts add-phone "+79261234567:Иван:Петров"
tgx contacts statuses          # кто когда был в сети, одним запросом
tgx contacts invite-token      # ссылка «добавь меня» без раскрытия номера
tgx contacts nearby 55.75 37.61            # только смотрим
tgx contacts nearby 55.75 37.61 --show-me 60   # и показываем себя на час
tgx contacts forget-top @кто-то   # убрать из «часто пишете»
```

`nearby` без `--show-me` **не публикует вас**: раздать своё местоположение
случайно проще, чем кажется, поэтому по умолчанию мы только смотрим.

Ссылку «добавь меня» сервер отдаёт готовой. Я сначала собирал её из токена сам —
и получал нерабочую: в настоящем токене есть вторая половина после двоеточия.

## Истории: остаток

```bash
tgx stories edit чат 42 --caption "новая подпись"
tgx stories views чат 42 43        # просмотры, реакции, пересылки
tgx stories reactions чат 42       # кто как отреагировал
tgx stories album чат 7            # что в альбоме
tgx stories pin-top чат 42         # наверх профиля
tgx stories hide-all on            # убрать чужие истории из ленты
tgx stories start-live чат         # прямой эфир; смотреть его нечем
```

У правки отсутствие поля значит «не трогать», а не «стереть»: не назвали
подпись — она осталась прежней.

## Мелочи по чатам

Каждая из них по отдельности не тянет на раздел, но искать их приходилось
каждый раз заново.

```bash
tgx message-link "AllCrew Cockpit" 42        # постоянная ссылка на сообщение
tgx message-link чат 42 --album              # на весь альбом
tgx common-chats @roman                      # где вы состоите вместе
tgx who-is чат @roman                        # кто он здесь: владелец, админ, гость
tgx my-public                                # ваши занятые публичные адреса
tgx similar durov                            # похожие каналы
tgx inactive                                 # где давно тихо — кандидаты на уборку
tgx antispam мойчат on                       # жёсткий антиспам (нужны бусты)
tgx default-ttl 604800                       # сообщения в новых чатах живут неделю
```

Даты в этих ответах приходят то числом, то датой — `inactive` отдаёт секунды,
остальное объекты. Приводим к одному виду, иначе в выводе оказывается
`1786542005` там, где ждёшь дату.

## Каналы и группы

Создание и администрирование — из интерфейса, из шелла и у агентов.

- **`n`** — новый чат: канал, супергруппа или группа с темами; название, описание
  и публичный адрес сразу в одном окне. Созданный чат тут же открывается.
- **`i`** — управление текущим чатом: название, описание, публичный адрес,
  права участников по умолчанию, слоу-мод, а также `ctrl+l` — ссылка-приглашение
  (копируется в буфер), `ctrl+d` — привязать группу обсуждения, `ctrl+u` — участники.
  Если вы не администратор, панель об этом честно пишет.

```bash
tgx channel-create "Мой канал" --kind channel --about "о чём" --username mychannel
tgx channel-create "Команда" --kind group          # или --kind forum, с темами
tgx channel-discussion @mychannel --group @myteam  # привязать обсуждение
tgx channel-permissions @myteam --allow send_messages,send_media,embed_links
tgx channel-slowmode @myteam 30
tgx chat-join https://t.me/+abcdef                 # или @username
tgx chat-leave @oldchat --yes
```

Уже были и остаются: `channel-info`, `channel-edit`, `channel-photo-set`,
`channel-participants`, `channel-admin-set/remove`, `channel-ban/unban`,
`channel-invite-add`, `invite-export/list`, `admin-log`.

Агентам доступны чтение (`chat_details`, `list_members`) и созидательная часть
записи: `create_chat`, `edit_chat`, `create_invite_link`, `set_slowmode`,
`set_permissions`. Назначение админов, баны, удаление канала и выход из чатов
намеренно оставлены только человеку — в CLI и TUI.

## Форматирование и посты

Входящие сообщения рендерятся со всем оформлением: жирный, курсив, подчёркнутый,
зачёркнутый, `код`, блоки кода, цитаты с полосой `▌`, упоминания и хэштеги
цветом. Ссылки — настоящие OSC-8 гиперлинки, в Ghostty/kitty/iTerm2 кликаются
мышью. Спойлеры закрыты плашкой `░░░`, открываются клавишей `s` на выделенном
сообщении.

**`p` — редактор поста**: слева разметка, справа живое превью того, что увидят
подписчики, снизу — файл, отложенная публикация, превью ссылок и тихая отправка.

Разметка (markdown, режим по умолчанию):

```
**жирный**   __курсив__   --подчёркнутый--   ~~зачёркнутый~~
`код`   ```блок кода```   ||спойлер||   [текст](https://ссылка)
> цитата — в начале строки, соседние строки склеиваются
```

Есть и HTML-режим: `<b> <i> <u> <s> <code> <pre> <blockquote> <tg-spoiler> <a href>`.
Отступление от Telethon: подчёркивание, спойлеры и цитаты его markdown не умеет —
tgx разбирает их сам и отправляет готовыми entity, поэтому то, что показано в
превью, ровно то и уходит.

Смещения считаются в **UTF-16**, как требует Telegram, — жирный текст после эмодзи
не съезжает.

Из CLI и у агентов то же самое:

```bash
tgx format                                   # шпаргалка
tgx format "**жир** ||секрет||"              # посмотреть, какие entity получатся
tgx send @channel "**Заголовок**\n\n> цитата" --schedule 2026-08-29T10:00
tgx send @channel "текст" --parse-mode html --no-preview --silent
```

## Вложения

`ctrl+s` — прикрепить файл. В диалоге: поле пути, дерево файлов для выбора мышью
или клавишами, превью с именем и размером (картинка рисуется тут же), четыре
переключателя и подпись.

| переключатель | что делает |
|---|---|
| как файл | без сжатия, оригиналом (`force_document`) |
| голосовое | ogg/opus уйдёт голосовым сообщением |
| кружок | круглое видео (video note) |
| без звука | уведомление без звука |

Тип Telegram определяет сам: картинка уйдёт фото, `.mp4` — видео с возможностью
стриминга, остальное — документом; переключатели это поведение перекрывают.
Для видео обязателен пакет `hachoir` (он в `requirements.txt`): без него Telethon
не может прочитать длительность и размер, подставляет заглушку 1×1, и Telegram
показывает ролик обычным файлом. Если в системе есть `ffmpeg`, tgx ещё и вырежет
кадр-обложку, чтобы у видео было превью.

- **несколько файлов** — перечислите пути через запятую, уйдут альбомом;
- **подпись** подставляется из поля ввода и правится прямо в диалоге;
- **ответ** — если выбран пост через `ctrl+r`, файл уйдёт ответом на него;
- **комментарий** — в канале, где вы не админ, файл уходит комментарием к посту;
- **прогресс** отправки виден в строке над полем ввода.

Из командной строки то же самое:

```bash
tgx send @peer "подпись" --file ~/photo.jpg --file ~/photo2.jpg
tgx send @peer --file ~/voice.ogg --voice
tgx send @peer --file ~/clip.mp4 --video-note --silent
tgx send @channel "мой коммент" --comment-to 12345
```

## Каналы и комментарии

В канале, где вы не админ, обычная отправка запрещена — но комментарии открыты.
Клиент это понимает: в шапке чата пишется `только комментарии`, поле ввода
меняет подпись, и текст из него уходит **комментарием** к посту, а не падает с
ошибкой прав.

- `c` — тред комментариев к посту: список с бабблами и своё поле ввода;
- под постом видно счётчик — `💬 12 комментариев   c — открыть`;
- к какому посту комментарий: к выделенному (стрелками или кликом), к тому, на
  который вы ответили через `ctrl+r`, иначе — к последнему.

Под капотом это `messages.SendMessage` с `comment_to` (Telethon сам находит
привязанную группу обсуждения) и `iter_messages(reply_to=…)` для чтения треда.
Если у поста комментарии выключены, будет понятное сообщение, а не трейсбек.

## Вложения в ленте

Долго не замечалось: **сообщение без подписи выглядело в ленте пустой строкой.**
Стикер, голосовое, фотография без текста — всё одинаково никак, и читать такую
ленту было невозможно. Теперь вложение всегда описано словами, даже когда его
нельзя показать:

```
[2026-08-28T22:49] Alex: [голосовое 0:03]
[2026-08-29T22:28] Alex: [видео 0:10]
[2026-08-29T22:35] Alex: [стикер 💯]
[2026-08-29T22:41] Роман: [файл отчёт.pdf 2.1 МБ]
[2026-08-29T22:44] Роман: [опрос: Идём?]
```

Подпись всегда важнее описания: если текст есть, показывается он, а вложение
остаётся отдельным полем. Кружок отличается от видео, голосовое от музыки, у
музыки виден исполнитель. Нулевой вес не печатается — это «неизвестно», а не
«0 Б».

## Прочитанное

Чат, в котором вы задержались дольше секунды, отмечается прочитанным — счётчик
непрочитанных гаснет и в TUI, и во всех остальных клиентах Telegram. Пролистывание
списка стрелками чаты не «съедает»: отметка ставится только если вы действительно
остались в чате. Отключается флагом `--no-mark-read`, вручную — `shift+r`.

## Медиа

Фото, видео, гифки и стикеры показываются картинкой прямо в бабле. Канал
рендера выбирается по возможностям терминала — **опрос делается на старте, до
запуска интерфейса** (позже Textual забирает stdin, и ответ терминала уже не
прочитать):

| терминал | канал | что видно |
|---|---|---|
| kitty, Ghostty, WezTerm | kitty graphics protocol | **настоящее изображение** |
| xterm, foot, Windows Terminal, iTerm2 | sixel | **настоящее изображение** |
| Terminal.app, tmux и всё остальное | полублоки `▀` в truecolor | цветной пиксель-арт, 2 пикселя на строку |

Что реально используется — видно в шапке: `250 чатов · медиа: kitty graphics`.
macOS Terminal.app графических протоколов не умеет вообще, там будут только
полублоки; за настоящими картинками — Ghostty, kitty, WezTerm или iTerm2.

Клавиши на выделенном сообщении:

| клавиша | действие |
|---|---|
| `v` | картинка на весь экран (у видео — кадр, крупнее чем в бабле) |
| клик / двойной клик | выделить сообщение · открыть картинку |
| `o` | открыть оригинал в системном просмотрщике |
| `ctrl+d` | скачать вложение в `data/downloads` |

Видео в терминале **не проигрывается** — ни kitty-протокол, ни sixel этого не
умеют: `v` покажет кадр, `o` скачает оригинал и отдаст его системному плееру
(с прогрессом в строке над полем ввода). Файлы кэша и загрузок сохраняются с
правильным расширением — иначе macOS не понимает тип и `open` молча
отказывается работать.

Флаги: `--media auto` (по умолчанию), либо `tgp` / `sixel` / `halfcell` /
`unicode` / `off`, либо `TGX_MEDIA`. Если картинки в терминале выглядят криво,
`--media halfcell` — самый безопасный вариант: обычный цветной текст, который
корректно скроллится.

В ленте качается **миниатюра** (~320 px, десяток килобайт), а не оригинал, и
только для последних 24 медиасообщений открытого чата; полноразмерная версия
подтягивается лениво при `v`. Кэш — `data/media-cache`, удаляется в любой
момент. Голосовые, аудио и документы без миниатюр остаются чипами.

## Анимации

Заставка сделана на [terminaltexteffects](https://github.com/ChrisBuilds/terminaltexteffects)
— 37 эффектов:

```bash
tgx banner --effect matrix     # или beams, decrypt, slide, laseretch, rain…
tgx banner --list              # все доступные
TGX_EFFECT=blackhole tgx ui    # эффект по умолчанию для запуска
```

Внутри интерфейса анимации свои: проявление бабблов, подсветка нового
сообщения, анимация «печатает…», плавное сворачивание панели, тосты.
`TEXTUAL_ANIMATIONS=none` отключает их, `TGX_NO_SPLASH=1` — заставку.

## CLI

Вывод раскрашивается только когда stdout — терминал. В пайпе, под `--plain`,
`NO_COLOR` или `TGX_PLAIN=1` формат остаётся ровно прежним, так что старые
скрипты не ломаются.

```bash
tgx dialogs --limit 30            # таблица
tgx history @channel --limit 50   # лента сообщений
tgx search "релиз" --per-dialog 3
tgx folders                       # папки Telegram
tgx folder-upsert AI --match-regex '(?i)\b(ai|llm)\b'
tgx send @user "привет" --file ./pic.png
tgx export @channel --output out.jsonl --format jsonl
```

Полный список — `tgx` без аргументов, детали по команде — `tgx <команда> --help`.

## Коннектор для агентов (MCP)

`bin/tgx-mcp` — MCP-сервер поверх того же аккаунта. Агенту доступно всё, что
доступно вам: **647 инструментов**.

```bash
claude mcp add tgx -- ~/telegram-cli-tools/bin/tgx-mcp
claude mcp add tgx --env TGX_MCP_READ_ONLY=1 -- ~/telegram-cli-tools/bin/tgx-mcp
~/telegram-cli-tools/bin/tgx-mcp --check                        # проверить руками
```

Набор состоит из двух частей.

**Именные инструменты** (около полусотни) написаны под частые задачи: у них
подробные описания, они делят одно соединение и отвечают быстрее.

**`cli_*` — весь командный интерфейс целиком.** Они не написаны, а выведены из
самого argparse: дерево команд обходится при старте, и каждая листовая команда
становится инструментом со своей сигнатурой, типами и подсказками из `help=`.
Новая команда в CLI появляется в MCP сама — дописывать в сервере нечего.

```
tgx profile photo клип.mp4 --start 2.5   →   cli_profile_photo(source, start=…)
tgx business connect @bot --replace      →   cli_business_connect(bot, replace=…)
```

Пропускаются только команды, которым нужен человек за клавиатурой: вход по коду
из SMS и запуск TUI — в переписке с агентом они бы просто зависли. Если одно имя
дают вложенная команда и старый плоский псевдоним, побеждает вложенная.

Ограничений по составу нет: профиль, бизнес-режим, токены ботов, удаление — всё
на месте. Опасное не запрещено, а помечено: инструменты удаления и выхода из
чата несут пометку `destructive`, а инструкции сервера требуют показать
пользователю, что именно сейчас произойдёт, и дождаться согласия.
`TGX_MCP_READ_ONLY=1` вешает замок, если он всё-таки нужен, а `TGX_MCP_PEERS`
сужает список чатов.

| инструмент | режим | что делает |
|---|---|---|
| `me` | чтение | аккаунт, и включена ли запись |
| `list_chats` | чтение | список чатов с фильтрами по типу, тексту, непрочитанным |
| `read_chat` | чтение | сообщения чата, с пагинацией и поиском |
| `search_messages` | чтение | поиск в чате или по всем чатам |
| `read_comments` | чтение | тред комментариев под постом |
| `list_folders` | чтение | папки со всеми правилами |
| `chat_info` | чтение | тип чата и можно ли в нём писать |
| `format_syntax` | чтение | шпаргалка по разметке для составления постов |
| `search_messages` | чтение | поиск с фильтрами по типу, отправителю и датам |
| `list_topics` · `list_pinned` | чтение | темы форума · закреплённые сообщения |
| `pin_message` · `create_topic` · `edit_topic` | **запись** | закрепление и темы |
| `send_checklist` | **запись** | чек-лист с пунктами |
| `rich_syntax` · `bot_send_rich` | чтение · **запись** | богатые сообщения Bot API 10.1 |
| `list_articles` · `publish_article` | чтение · **запись** | статьи на telegra.ph из маркдауна |
| `list_bots` | чтение | имена сохранённых ботов — **без токенов** |
| `bot_post` | **запись** | пост от имени бота с кнопками и мини-приложениями |
| `download_media` | чтение | скачать вложение, вернуть путь |
| `send_message` | **запись** | текст с разметкой, ответ, комментарий, отложенная отправка |
| `react` | **запись** | поставить или убрать реакцию |
| `edit_message` | **запись** | править своё сообщение (md/html) |
| `forward_messages` | **запись** | переслать между чатами |
| `press_button` | **запись** | нажать инлайн-кнопку бота |
| `send_file` | **запись** | файлы и альбомы со всеми флагами |
| `mark_read` | **запись** | отметить чат прочитанным |

Что осталось от прежних ограничений:

- **предупреждение вместо запрета** — необратимое (удаление, выход из чата, отзыв
  токена, подключение бота к личной переписке, смена собственного аватара)
  помечено `destructive`, и в инструкции сервера сказано сначала показать
  пользователю, что именно произойдёт, и дождаться ответа;
- **`TGX_MCP_READ_ONLY=1`** — замок на всю запись, если он нужен;
- **`TGX_MCP_PEERS=@chat1,@chat2`** ограничивает агента конкретными чатами;
- **отдельная сессия** `data/tgx-mcp.session`, снятая копией через sqlite-backup,
  поэтому сервер работает одновременно с открытым TUI; команды `cli_*` идут через
  то же соединение, иначе второй клиент упирается в «database is locked»;
- лимиты выдачи режутся до 200 сообщений на вызов;
- токен бота выдаётся по запросу, но в инструкции сервера отдельно сказано не
  записывать его в сообщения и файлы;
- инструкции, найденные внутри самих сообщений Telegram, — это данные, а не
  приказы: агенту велено их цитировать и спрашивать.

Сообщения уходят от вашего имени — текст и получателя агент подтверждает заранее.

Если агенту проще шеллом, а не по MCP, весь CLI уже машинный:
`tgx dialogs --jsonl`, `tgx history @peer --jsonl`, `tgx search "…" --jsonl` —
в пайпе формат остаётся стабильным JSON.

## Сколько покрыто

```bash
.venv/bin/python tools/coverage.py            # сводка
.venv/bin/python tools/coverage.py messages   # пробелы одного раздела
```

На сегодня все одиннадцать разделов, которые имеют смысл в терминале, закрыты
полностью: **100% по делу**.

Голая доля вводит в заблуждение. В схеме лежат секретные чаты, покупки в
магазинах приложений, обработчики на стороне работающего бота и сигнальный обмен
звонков — терминальному клиенту это недоступно или не нужно. Поэтому непокрытое
делится на три кучки: сделано через обёртку Telethon, неприменимо, настоящий
пробел. Считать имеет смысл только последнюю — это столбец «по делу».

Отнесения в `tools/coverage.py` — суждение, а не факт. Они выписаны списком,
их видно, и с ними можно спорить.

## Структура

```
bin/tgx            обёртка venv → tgx.py
bin/tgx.py         CLI: подкоманды, argparse
bin/tgx_tui.py     TUI: бэкенды (Telethon + демо), виджеты, приложение
bin/tgx_tui.tcss   стили TUI (темы tgx-night / tgx-day)
bin/tgx_article.py статьи telegra.ph: маркдаун → узлы, публикация
bin/tgx_business.py бизнес-режим: бот-секретарь, часы, автоответы, ссылки
bin/tgx_forum.py    форумы и темы целиком: правила сервера в одном месте
bin/tgx_guard.py    именные одноразовые приглашения и сверка вошедших
bin/tgx_poll.py     опросы и викторины, включая новинки Bot API 10.0
bin/tgx_pay.py      звёзды, TON и счета; оплата — за воротами подтверждения
bin/tgx_confirm.py  человек в контуре: карточка с кнопками и ожидание решения
bin/tgx_contacts.py адресная книга, чёрный список, поиск людей
bin/tgx_stories.py  истории: лента, публикация, просмотры, альбомы
bin/tgx_folders.py  общие папки: ссылки на набор чатов и их обновления
bin/tgx_stickers.py свои наборы стикеров
bin/tgx_stats.py    статистика каналов, групп, постов и историй
bin/tgx_pending.py  черновики, отложенные, заготовки, избранное
bin/tgx_security.py сессии, приватность, сроки
bin/tgx_calls.py    групповые звонки: управление из терминала
bin/tgx_callweb.py  живая страница участников звонка
bin/tgx_transcribe.py расшифровка голосовых: ожидание готового текста
bin/tgx_profile.py  оформление: аватары всех форматов, цвета, статус, дата
bin/tgx_banner.py   запись терминальной заставки в видео для аватара
bin/tgx_ai.py      руками Telegram: правка, пересказ, перевод, тоны письма
bin/tgx_takeout.py выгрузка аккаунта: контакты, чаты, история, вложения
bin/tgx_chatx.py   мелочи по чатам: ссылки, общие группы, антиспам, сроки
bin/tgx_triage.py  что ждёт внимания: упоминания, реакции, прочтения, счётчики
bin/tgx_notify.py  уведомления и тишина, заявки на вступление, набор реакций
bin/tgx_safety.py  блокировки, жалобы по меню сервера, уборка переписки
bin/tgx_groups.py  обычные группы, ветки обсуждений, признак набора
bin/tgx_chanadmin.py остаток управления каналами: адреса, вид, уборка
bin/tgx_webapp.py  окно вокруг мини-приложения: протокол хозяина
bin/tgx_windows.py реестр окон и мостик к ним для агентов
bin/tgx_palette.py все команды CLI внутри полноэкранного клиента
bin/tgx_proxy.py   мини-приложение через свой адрес: видимость целиком
bin/tgx_account.py настройки и оформление аккаунта: адреса, обои, темы
bin/tgx_msgextra.py остаток по сообщениям: голоса, факт-чек, предпросмотр
bin/tgx_inline.py  чужие боты: инлайн-запросы, кнопки, мини-приложения
bin/tgx_autotools.py вывод инструментов MCP из дерева команд CLI
bin/tgx_net.py     общий HTTPS: сертификаты и понятные ошибки сети
bin/tgx_rich.py    богатые сообщения: разметка, лимиты, отправка и отрисовка
bin/tgx_bots.py    боты: реестр токенов, диалог с BotFather, клиент бота
bin/tgx_format.py  разметка: разбор в entity, отрисовка, шпаргалка
bin/tgx_media.py   превью картинок: выбор протокола и размера в ячейках
bin/tgx_render.py  общий слой вывода: палитра, таблицы, JSON, лента сообщений
bin/tgx_splash.py  анимированная заставка
bin/tgx-mcp        обёртка venv → tgx_mcp.py
tools/coverage.py  что из API покрыто и почему остальное — нет
bin/tgx_mcp.py     MCP-коннектор для агентов (чтение по умолчанию)
bin/tgx_smoke.py   headless-самопроверка TUI + SVG-скриншоты
```

Проверка после правок:

```bash
.venv/bin/python bin/tgx_smoke.py /tmp/tgx-shots
```