agentleFS
dewildewil/claude-toolkit/skills/telegram-send/SKILL.md

telegram-send

Skill9 starsChanged 3 months agoMITView on GitHub

Отправить сообщение в Telegram-чат от имени пользователя (личка или группа): \"напиши в телеграм\", \"отправь в чат X\", \"скинь в группу\". НЕ для чтения истории - скилл `telegram-snapshot`.

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Files
SKILL.md

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

# превью (ничего не отправляет):
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
# один документный альбом (повтори --file для каждого файла):
python3 scripts/telegram-send.py --to "с командой" --file "путь/к/file-1.pdf" --file "путь/к/file-2.pdf" --text "..." --send

Флаги:

  • --to <label> (обязательный) - label чата из .telegram-snapshot.json -> chats. Если label нет - скрипт печатает список доступных и выходит.
  • --text <str> - текст. Если опущен, читается из stdin (удобно для многострочного).
  • --file <путь> - файл-вложение (отправляется документом, force_document); текст становится подписью. Повтори флаг 2-10 раз, чтобы отправить один альбом одним вызовом. Все файлы предварительно проверяются; пустые, недоступные, повторные (включая hardlink) и альбомы больше 10 отклоняются до auth/client/network. Альбомы пока несовместимы с --schedule, --voice и --html; подпись ограничена 1024 UTF-16 code units. С одним файлом сохраняется прежний одиночный путь и его режимы. 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 символами. Для объемного текста - отправить файл с короткой подписью, затем текст отдельным сообщением. Для альбома подпись также ограничена 1024 UTF-16 code units и прикрепляется только к первому документу. Альбом уходит одним send_file вызовом; returned IDs показываются до записи pacing. При ошибке отправки, частичном подтверждении или post-send cleanup статус НЕОПРЕДЕЛЕННО (код 4): не повторяй автоматически, сначала проверь свежую историю чата. Живая проверка требует отдельно разрешенного тестового чата; без нее отмечай NOTRUN и не заявляй live acceptance.

Когда нужен --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 - явное исключение из правила выше:

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

python3 scripts/telegram-send-one.py <chat_id> <expected_username> \
  --file "путь/к/файлу.pdf" --text "короткая подпись" --send
# документный альбом: повтори --file для каждого документа
python3 scripts/telegram-send-one.py <chat_id> <expected_username> \
  --file "путь/к/file-1.pdf" --file "путь/к/file-2.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 - первичная настройка авторизации и подключение чатов в конфиг.

Модель и текущие назначения

Перед новым заданием и запуском применяй маршрутизацию моделей: свежий MODELS выше канонического дефолта. Границы данных и полномочий этого навыка сохраняются.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.