Отправить сообщение в Telegram-чат от имени пользователя (личка или группа): "напиши в телеграм", "отправь в чат X", "скинь в группу". НЕ для чтения истории - скилл `telegram-snapshot`.
Installs into .claude/skills of the current project.
Are you the author of Telegram Send?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dewil-telegram-send)
---
name: telegram-send
description: Отправить сообщение в Telegram-чат от имени пользователя (личка или группа): "напиши в телеграм", "отправь в чат X", "скинь в группу". НЕ для чтения истории - скилл `telegram-snapshot`.
---
# telegram-send
Скилл для отправки сообщения в Telegram-чат проекта **от имени пользователя**. Личка и группа отправляются одинаково. Скрипт-эталон - `scripts/telegram-send.py` (top-level папка `scripts/` в корне проекта). Авторизация общая с `telegram-snapshot` (`~/.config/telegram-snapshot/auth.json` + `.session`), адресаты берутся из `.telegram-snapshot.json`.
## Когда применять
- Пользователь просит написать/отправить/скинуть сообщение в один из чатов проекта от своего имени.
- Нужно ответить в конкретную форумную тему или реплаем на сообщение.
## Когда НЕ применять
- Чтение/анализ истории чатов - это `telegram-snapshot` (`result.json` уже на диске, читай напрямую).
- Адресата нет в `.telegram-snapshot.json` - сначала добавить его туда (см. ниже), отдельного механизма отправки "куда угодно" у скрипта нет.
## Обязательный гейт отправки
Отправка в Telegram **необратима и видна другому человеку**. Поэтому всегда:
1. **Dry-run.** Запусти `scripts/telegram-send.py` БЕЗ `--send`. Скрипт зарезолвит адресата и напечатает заголовок чата, id, тему/реплай и полный текст - но ничего не отправит.
2. **Покажи пользователю** результат dry-run: в какой именно чат (по заголовку, а не только label) и **текст дословно**, как он уйдет.
3. **Дождись явного "ок".** Без подтверждения - не отправлять. Это конкретизация канон-принципа "audit -> ок -> action" для внешнего необратимого действия.
4. **Только потом** запусти ту же команду с `--send`.
Никогда не запускай `--send` без показанного пользователю dry-run и явного согласия. Молчаливое согласие ("ну напиши что-нибудь") - не основание отправлять не показав текст.
**Гейт темпа - второй предохранитель, уже про серию.** Отправка слишком рано после предыдущей в тот же чат отклоняется с кодом 3: "нужно N сек, осталось M". Это **сработавшее правило, а не поломка** (`rules/outbound-timing.md`, "Паузы внутри своей серии"): подожди названное время и повтори ту же команду. Dry-run показывает состояние гейта заранее строкой "темп: рано - ...", так что упереться в него на реальной отправке необязательно.
Обход - `--no-pace-check`, и он уместен ровно в двух случаях: на том конце не человек (бот, свой канал, служебный чат) или есть срочность по существу. "Мне некогда ждать" в этот список не входит - залп сообщений выдает автоматику вернее, чем содержание текста.
## Как составлять текст
Голос - по правилу `rules/agent-as-user-voice.md`: держать фактический речевой регистр пользователя, снятый из истории переписки в этом чате, а не причесанный ассистентский. Это его сообщение, не твое, и переполировка выдает бота.
Время отправки - по `rules/outbound-timing.md`: у сообщения видна метка времени, и ночная отправка выдает автомат не хуже стиля. Показывая текст на подтверждение (см. "Жесткие правила"), скажи и когда отправляешь - сейчас или в рабочее окно получателя.
Специфика отправки:
- Не добавляй подпись "отправлено через..." и подобное.
- Если пользователь продиктовал текст дословно - отправляй дословно, не переписывай.
- Перед отправкой показываешь ровно тот текст, что уйдет (никаких "примерно так").
## CLI
```bash
# превью (ничего не отправляет):
python3 scripts/telegram-send.py --to "с командой" --text "Привет, созвон в 15:00"
# реальная отправка (после "ок"):
python3 scripts/telegram-send.py --to "с командой" --text "Привет, созвон в 15:00" --send
# многострочный текст - через stdin:
python3 scripts/telegram-send.py --to "с командой" --send <<'EOF'
Первая строка.
Вторая строка.
EOF
# в форумную тему:
python3 scripts/telegram-send.py --to "бот" --topic 30127 --text "..." --send
# ответом на сообщение (ответ попадет в тему этого сообщения):
python3 scripts/telegram-send.py --to "бот" --reply-to 4821 --text "..." --send
# файл-вложение (текст уходит подписью к файлу):
python3 scripts/telegram-send.py --to "с командой" --file "путь/к/архиву.zip" --text "..." --send
```
Флаги:
- `--to <label>` (обязательный) - label чата из `.telegram-snapshot.json -> chats`. Если label нет - скрипт печатает список доступных и выходит.
- `--text <str>` - текст. Если опущен, читается из stdin (удобно для многострочного).
- `--file <путь>` - файл-вложение (отправляется документом, `force_document`); текст сообщения становится подписью к файлу. Если ни `--text`, ни stdin не заданы, файл уйдет без подписи. Пустой `--file ""` или путь к несуществующему файлу в обоих скриптах отправки - отказ с кодом 2 до создания клиента, и в dry-run, и с `--send`. Dry-run показывает полный резолвленный путь и точный размер в байтах - по одному имени не проверить, тот ли это файл.
- `--send` - реально отправить. Без него - dry-run.
- `--topic <int>` - id корня форумной темы. Если не задан, но у label в конфиге расширенная запись с `topic_id` - берется он (так сообщение в форум-чат уходит в нужную тему, а не в General).
- `--reply-to <int>` - id сообщения, на которое отвечаем.
- `--silent` - отправить без звука: уведомление придет беззвучным. Есть у обоих скриптов.
- `--schedule <ISO-время>` - отложить до этого момента (см. "Когда нужен `--schedule`" ниже). Запас до срока - не меньше двух минут: меньший скрипт отклоняет сам, до сети, потому что Telegram при запасе меньше десяти секунд отправляет немедленно, а подключение и загрузка файла запас съедают; секунды в превью печатаются. Есть у обоих скриптов.
- `--schedule-tz <IANA>` - пояс получателя по имени (`Europe/Paris`) для `--schedule`; без `--schedule` - ошибка (см. "Когда нужен `--schedule`" ниже). Есть у обоих скриптов.
- `--remind` - только `telegram-send-one.py` и только адрес `me`: отложить на ~3 минуты, чтобы Telegram показал уведомление (см. "Сообщение себе" ниже). Сам снимает гейты темпа и ровной минуты; с `--silent`, `--schedule`, `--schedule-tz` и другим адресатом - отказ кодом 2.
- `--exact-minute` - разрешить ровную минуту (`:00`/`:15`/`:30`/`:45`, независимо от секунд) в `--schedule`; без него такая минута отклоняется кодом 6 (см. "Когда нужен `--schedule`" ниже). Есть у обоих скриптов.
`--reply-to` отвечает на сообщение и кладет ответ в тему этого сообщения автоматически; `--topic` нужен для постинга нового сообщения в тему без реплая. Если задать оба, приоритет у `--reply-to` (тема берется из отвечаемого сообщения, `--topic` игнорируется) - high-level API Telethon не умеет одновременно reply на сообщение и явный topic.
**Когда нужен `--silent`.** Это средний путь между "отправить сейчас" и "отложить до утра" из `rules/outbound-timing.md`: сообщение готово, срочности нет, но и ждать утра незачем - асинхронный тред, чат сообщества, получатель прочитает когда откроет. Ночной звонок при этом никого не будит. Важно понимать границу: беззвучность снимает уведомление, но **не** ночную метку времени - она остается видимой. Поэтому там, где сам таймстемп читается как сигнал (отклик на вакансию, письмо рекрутеру, первое сообщение незнакомому человеку), флаг правило не заменяет: ждем рабочее окно. Dry-run печатает `звук: да/нет` - проверяйте это перед `--send` вместе с адресатом.
Подпись к файлу (`--file`) ограничена Telegram ~1024 символами: длинный текст к вложению не влезет, и `--send` упадет с `MediaCaptionTooLongError`, ничего не отправив. Для объемного текста - отправить файл с короткой подписью, затем текст отдельным сообщением. Один файл за вызов; альбомы/несколько вложений - вне скоупа.
**Когда нужен `--schedule`.** Это настоящее "отложить до утра" из `rules/outbound-timing.md`, а не ручная эмуляция ("подготовил, жду, потом нажал отправить"): время передается `--schedule <ISO-время>`, и сообщение ложится в очередь на серверах Telegram. Уходит оно само, в назначенный момент, без запущенного скрипта и открытой сессии. До срока сообщение видно в "Отложенных" у отправителя, и его можно оттуда отозвать; получатель видит время доставки, а не время постановки в очередь.
Оффсет задавай по поясу получателя, когда он известен (`rules/outbound-timing.md`, "Норма") - `2026-09-22T09:30+03:00`; без оффсета скрипт берет зону машины, и это фолбэк на случай, когда пояс получателя не восстановить, а не выбор по умолчанию. При переходе на летнее время локальное время без оффсета может оказаться неоднозначным (два момента подряд) или несуществующим (такого момента в этом дне нет) - скрипт такое отклоняет и просит задать оффсет явно, вместо того чтобы подставлять его сам.
**Получатель в поясе с переходом часов - всегда `--schedule-tz`, а не число.** Числовой оффсет верен только на ту дату, на которую его считали: команда с `+02:00`, повторенная после перевода часов, молча доставит сообщение на час раньше. С `--schedule-tz Europe/Paris` время без оффсета читается как настенное время в этом поясе, а оффсет берется из базы поясов на дату доставки (`2026-11-02T09:06` станет `+01:00`, `2026-10-20T09:06` - `+02:00`). Пропущенный или повторенный час в этом поясе отклоняется так же, как для зоны машины. Явный оффсет вместе с флагом допустим, но обязан совпасть с поясом на этот момент, иначе отказ с обеими цифрами. Неизвестное имя пояса - отказ до сети. Dry-run и строка результата печатают время дважды: у получателя и в зоне машины - `отложено до: 2026-11-02 09:06:00 +01:00 Europe/Paris (в зоне машины: 2026-11-02 11:06:00 +03:00)`. Без флага вывод прежний.
Dry-run печатает получившееся время с оффсетом и секундами (`отложено до: 2026-09-22 09:30:00 +03:00`) - проверь его до `--send`, если оффсет не задан явно: ошибка зоны машины иначе всплывет только в момент доставки. Прошлое время, время дальше года вперед (лимит отложенной отправки в Telegram) и запас до срока меньше двух минут скрипт отклоняет сразу, до сети.
**Ровная минута отклоняется отдельно, кодом 6.** `:00`, `:15`, `:30`, `:45` независимо от секунд читаются как подпись автомата, а не человека (`rules/outbound-timing.md`, "Ровная минута") - скрипт отказывает до сети, и в dry-run, и с `--send`, с подсказкой соседней некруглой минуты. `09:00:30` остается круглой минутой: секунды не служат обходом. Минуту назвал сам пользователь явно ("ровно в 9:00", "к началу созвона") - обход `--exact-minute`; время доставки сохраняет исходные секунды, а dry-run печатает пометку `ровная минута: разрешена явно`.
**Сообщение себе (`me`, Избранное) уведомления не дает.** Telegram считает его исходящим: оно ложится в Избранное молча, с `--silent` и без него. Отчет по расписанию, который человек должен заметить, шлется отложенным - `telegram-send-one.py me --remind --send`: отложенное себе Telegram доставляет как напоминание, со звуком на всех устройствах (проверено 29.09.2026). Ночью, когда будить не надо, - прямое сообщение без `--remind`.
Для Избранного у отложенного есть второе отличие: на близком сроке Telegram не показывает его в очереди отложенных (на сроке в сутки - показывает). Поэтому для себя "не найдено в очереди" - не сбой: скрипт отвечает кодом 0 и пишет, что сверка для Избранного на близком сроке невозможна. Для любого другого чата "не найдено" по-прежнему код 4.
## Адресаты только из конфига
Скрипт шлет строго в чаты, перечисленные в `.telegram-snapshot.json`. Это намеренное ограничение - предсказуемость и защита от отправки не туда.
Чтобы написать в новый чат, сначала добавь его label в `.telegram-snapshot.json -> chats` (формат записи и как узнать `chat_id` - см. скилл `telegram-snapshot`, шаг 6). После этого `--to <новый label>` заработает.
### Single-chat-by-id: адресат вне конфига
Для сценариев, где адресата в общий конфиг класть нельзя (разовый чат, клиент трека поддержки - его чат не должен попадать в ежедневные pull/дельты), есть драйвер `scripts/telegram-send-one.py` - явное исключение из правила выше:
```bash
# dry-run (по умолчанию), id прямо в командной строке:
python3 scripts/telegram-send-one.py <chat_id> [expected_username] --text "..."
# реальная отправка (после "ок"):
python3 scripts/telegram-send-one.py <chat_id> [expected_username] --text "..." --send
```
Тот же гейт dry-run/`--send`, что у основного скрипта. Вместо защиты "только из конфига" - опциональная сверка `expected_username`: если username чата не совпал с ожидаемым или отсутствует, отправка прерывается с кодом 2 (страховка от "не туда"). Явно переданный пустой `expected_username` - ошибка ввода с кодом 2 до создания клиента. Флаг `--html` включает HTML-форматирование (жирный/код/ссылки); HTML, а не MarkdownV2 - тот требует экранировать точки/дефисы/скобки, на русском тексте это грабли. Симметричный pull одного чата по id - `scripts/telegram-pull-one.py` (см. скилл `telegram-snapshot`).
Вложение - как у config-версии, флаг `--file`:
```bash
python3 scripts/telegram-send-one.py <chat_id> <expected_username> \
--file "путь/к/файлу.pdf" --text "короткая подпись" --send
```
Для чата вне `.telegram-snapshot.json` это единственный штатный способ отправить файл - править общий конфиг ради одной отправки не нужно. Текст уходит подписью к файлу (лимит Telegram - 1024 символа: длиннее - двумя сообщениями, сначала текст, потом файл с короткой подписью; скрипт при превышении останавливается сам). Файл без подписи легален; dry-run показывает полный путь и размер вложения, существование файла проверяется до захвата общей сессии. Сверка `expected_username` работает и здесь.
## Несколько аккаунтов
Отправитель определяется чатом: у записи чата в `.telegram-snapshot.json` может быть поле `account` (без него - `default`), и сообщение уходит от того аккаунта, которому чат принадлежит. Dry-run печатает строку `от аккаунта: <имя>` - при нескольких номерах ошибиться отправителем так же легко, как чатом, поэтому проверяй ее наравне с заголовком чата.
Для драйвера вне конфига аккаунт задается флагом: `telegram-send-one.py <id> --account cv --text "..."`.
Настройка аккаунтов (секция `accounts` в `auth.json`, наследование `api_id`/`proxy`, первый логин) описана в скилле `telegram-snapshot`, раздел "Несколько аккаунтов". Важно: **залогинить новый аккаунт можно только через snapshot** - этот скрипт неинтерактивен и на неавторизованной сессии просто откажет с сообщением про `.session`.
Если чат не резолвится ("не удалось найти чат") - проверь поле `account`: чат виден только своей сессии.
## Авторизация
Та же, что у `telegram-snapshot`: `~/.config/telegram-snapshot/auth.json` (api_id, api_hash, session_name) + общая `.session`. Отдельной настройки для отправки не нужно - если snapshot уже работает на устройстве, работает и send. При проблемах с авторизацией/сессией (`AuthKeyUnregisteredError`, PeerUser-ошибки, нет auth.json) - см. скилл `telegram-snapshot`.
Сессия общая, поэтому отправка может упасть с `sqlite3.OperationalError: database is locked`, если параллельно работает snapshot другого проекта. Подключение скрипт повторяет сам; если ошибка все же дошла - правило поведения в скилле `telegram-snapshot`, раздел "Общая сессия: `database is locked`": ждать и повторять, чужой процесс не убивать, `.session` не удалять.
**Важно про дубли при повторе.** Слепой повтор отправки недопустим - смотри, где именно упало. Три случая, а не два:
- **упало на подключении** (в трейсбеке `connect_with_retry`, ниже - кадр telethon `sessions/sqlite.py`, `_update_session_table`) - до отправки, сообщение НЕ ушло, повтор безопасен;
- **напечатано `OK: отправлено ...`** - сообщение ушло, повторять нельзя. Ошибка после этой строки относится к чему-то после отправки (например, закрытию клиента), а не к самой отправке;
- **упало между этим, и в трейсбеке есть кадр `send_message` / `send_file`** - **статус неизвестен, и это самый опасный случай**. Telethon пишет сессию уже ПОСЛЕ того, как Telegram принял сообщение (кадр `client/users.py` под кадром отправки), поэтому `database is locked` здесь означает "скорее всего доставлено, но скрипт этого не подтвердил". Не повторять: сначала убедиться, ушло ли (`telegram-pull-one.py` по этому чату, свежий snapshot или глазами пользователя), и только потом решать. Не уверен - спросить пользователя, а не отправлять второй раз.
Кадр `client/users.py` **без** кадра отправки к этому случаю не относится: через него идет любой запрос, включая прогрев диалогов и резолв чата до отправки. Нет в трейсбеке `send_message`/`send_file` - отправка не начиналась, повтор безопасен.
**У `--schedule` дубли проверяются иначе.** Обрыв соединения или отдельный ненулевой код "неопределенно" (постановка в очередь не подтверждена) - смотри "Отложенные" у отправителя, а не историю чата: до срока доставки отложенное сообщение в истории не появляется, и ее пустота ничего не доказывает. Сообщение в "Отложенных" нашлось - повторный вызов поставит второе, дедупликации на постановке в очередь нет. Код "неопределенно" значит то же, что и в случае выше: проверь руками, не повторяй автоматически.
## Жесткие правила
1. **Без dry-run и явного "ок" - не отправлять.** Всегда показать пользователю заголовок чата и текст дословно перед `--send`.
2. **Только чаты из `.telegram-snapshot.json`** - либо `telegram-send-one.py` со сверкой `expected_username` для адресата вне конфига (см. "Single-chat-by-id"). Нет label и не single-chat-сценарий - не отправлять, предложить добавить в конфиг.
3. **Текст - от имени пользователя, дословно по согласованию.** Не переписывать продиктованное, не дописывать от себя.
4. **Auth и .session - не в проектной папке.** Только `~/.config/telegram-snapshot/` (см. `telegram-snapshot`).
## Связанные файлы
- `scripts/telegram-send.py` в корне проекта - эталонный скрипт отправки (dry-run по умолчанию, `--send` для реальной отправки).
- `scripts/telegram-send-one.py` - драйвер single-chat-by-id: отправка в один чат по id, минуя конфиг (тот же гейт, сверка username, `--html`, `--file`, `--silent`). Переиспользует логику telegram-send.py через импорт.
- `.telegram-snapshot.json` в корне проекта - справочник адресатов `{label: chat_id}` (общий со snapshot/deltas).
- `~/.config/telegram-snapshot/auth.json` + `.session` - общая авторизация на устройство.
- Скилл `telegram-snapshot` - первичная настройка авторизации и подключение чатов в конфиг.