Skip to content
Back to skills

Telegram Snapshot

ASecurity

Настроить или починить инкрементальный pull Telegram-чатов на устройстве: "my.telegram.org выдает ERROR", "telethon не видит чаты". НЕ для отправки сообщений - скилл `telegram-send`.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 29, 2026
developmentpythonbashsqlapidatabase

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 3, 2026

npx -y skills add dewil/claude-toolkit --skill telegram-snapshot --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Telegram Snapshot?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Telegram Snapshot
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dewil-telegram-snapshot/badge)](https://www.skillsdirectory.com/skills/dewil-telegram-snapshot)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: telegram-snapshot
description: Настроить или починить инкрементальный pull Telegram-чатов на устройстве: "my.telegram.org выдает ERROR", "telethon не видит чаты". НЕ для отправки сообщений - скилл `telegram-send`.
---

# telegram-snapshot

Скилл для первой настройки или починки автоматизированного pull Telegram-чатов проекта через Telethon. Фиксирует **накопленный опыт по граблям**, на которые натыкаешься в первый раз.

Скрипты-эталоны лежат в каноне в `scripts/telegram-snapshot.py` и `scripts/telegram-deltas.py` (top-level папка `scripts/`, не внутри папки скилла). `.AI` bootstrap типа management копирует их в `scripts/` в корне проекта - оттуда же, где их ожидает скилл при запуске (`python3 scripts/telegram-snapshot.py`). В эталонной версии уже есть `iter_dialogs()` прогрев (см. шаг 5).

## Когда применять

- Пользователь впервые настраивает скрипты `scripts/telegram-snapshot.py` / `telegram-deltas.py` на новом устройстве.
- my.telegram.org возвращает голое `ERROR.` при создании приложения и не дает api_id/api_hash.
- Telethon ругается `Could not find the input entity for PeerUser(...)` на свежей сессии.
- Сессия отозвана (`AuthKeyUnregisteredError`), нужно перелогиниться.
- Подключается новый проект - нужно создать `.telegram-snapshot.json` и проверить, что общая авторизация на устройстве уже есть.

## Когда НЕ применять

- Обычный регулярный pull (запуск `telegram-snapshot.py` для свежих сообщений) - это просто Bash-команда из шаблона дейлика, отдельный скилл не нужен.
- Любые задачи по экспорту/анализу самих сообщений - они уже в `result.json`, читай напрямую.

## Алгоритм

### Шаг 1. Проверить общую авторизацию устройства

```bash
ls ~/.config/telegram-snapshot/
```

Должно быть:
- `auth.json` (api_id, api_hash, session_name) - права 600.
- `<session_name>.session` - SQLite-сессия Telethon.

Если файлов нет - идем к шагу 2. Если `auth.json` есть, а `.session` нет - идем к шагу 4 (первый интерактивный логин).

### Шаг 2. Получить api_id и api_hash

**Сначала пробовать свои через my.telegram.org**.

Если my.telegram.org возвращает `ERROR.` при "Create application" - это блокировка на стороне аккаунта (антифрод по новизне аккаунта и/или IP VPN). По состоянию на май 2026 воспроизведено на разных комбинациях полей формы (platform=desktop/other, разные title/shortname, с url и без) - сервер режет одинаково. Лечится:
1. Сменой VPN-узла на другую страну/провайдера (см. список ниже).
2. Ожиданием 6-24 часа после неудачных попыток.
3. Использованием другого, более старого активного Telegram-аккаунта.

VPN-предпочтения для my.telegram.org: лучше Германия/Нидерланды/Финляндия, хуже США/UK. Резиденциальные IP > datacenter. Свой WireGuard на личном VPS почти всегда проходит.

**Fallback - публичные ключи Telegram Desktop OSS**:
- `api_id`: `17349`
- `api_hash`: `344583e45741c457fe1862106095a5eb`

Это публичные значения из исходников tdesktop. Технически серая зона: ToS Telegram требует получать собственный api_id, гарантий не давать банов нет. В личной некоммерческой практике банов не наблюдалось, но ответственность на пользователе - надежнее получить свой api_id (см. жесткое правило 4).

### Шаг 3. Заполнить auth.json

```bash
mkdir -p ~/.config/telegram-snapshot
chmod 700 ~/.config/telegram-snapshot
```

Содержимое `~/.config/telegram-snapshot/auth.json` (выставить права 600):
```json
{
  "api_id": 17349,
  "api_hash": "344583e45741c457fe1862106095a5eb",
  "session_name": "<имя_аккаунта>"
}
```

`session_name` - произвольное (например `dwl`), под этим именем будет файл `<name>.session` в той же папке.

Опциональное поле `proxy` - для хостов, где прямой доступ к Telegram режется (см. "Запуск с сервера / прокси").

### Шаг 4. Поставить telethon и сделать первый запуск

```bash
pip3 install --user telethon
cd <project-root>
python3 scripts/telegram-snapshot.py
```

Первый запуск **обязательно интерактивный** - в Bash из этого скилла НЕ запускать (stdin недоступен). Сказать пользователю запустить в его терминале и продиктовать ввод:

- Phone: `+7...` (международный формат).
- Code: из приложения Telegram (приходит в Saved Messages от "Telegram").
- Password: 2FA, если включена.

После успеха появится `~/.config/telegram-snapshot/<session_name>.session`. Дальше запуски без ввода - **в любом проекте на этом устройстве**.

### Шаг 5. Лечение типичных ошибок при первом pull

**`Could not find the input entity for PeerUser(...)` на нормальных групповых чатах.** На свежей сессии telethon без подсказки трактует голый int chat_id как user_id. Лечится прогревом - перед циклом обработки чатов в `amain()` нужно пройтись по диалогам через `client.iter_dialogs()`. В каноне это совмещено с построением карты `{unmarked_id -> entity}`:

```python
dialog_entities = {}
async for d in client.iter_dialogs():
    eid = getattr(d.entity, "id", None)
    if eid is not None:
        dialog_entities[eid] = d.entity
```

Карта нужна и для прогрева, и как авторитетный источник entity (см. следующий пункт про битый кеш). В канонической версии `scripts/telegram-snapshot.py` этот блок уже есть. Если в проекте старая версия скрипта без прогрева `iter_dialogs()` - обновить клиент с `.AI` через `/canon` и `canon-sync`; legacy-only клиент сначала требует отдельного запроса на миграцию в `.AI`.

**Один и тот же `chat_id` резолвится в РАЗНЫЕ чаты между запусками (битый entity-cache).** На некоторых сессиях `get_entity(<голый int>)` нестабилен: локальный кеш entity повреждается и тот же id отдается то одним, то другим чатом. Симптом - снапшоты "уезжают" не в свои папки. Лечится тем, что entity берется не из `get_entity`, а из свежего серверного списка диалогов: в `amain()` цикл `iter_dialogs()` строит карту `{unmarked_id -> entity}`, и `process_chat` резолвит чат через нее (`resolve_entity`), а `get_entity` оставлен только фолбэком для чатов вне списка диалогов. В канонической версии это уже сделано. При сомнениях, что метки папок верны, сверяй заголовки чатов через `iter_dialogs`, а не доверяй `get_entity`.

**`AuthKeyUnregisteredError` / сессия отозвана.** Удалить `~/.config/telegram-snapshot/<session_name>.session` и перезапустить - пройдет авторизация с нуля.

**`Server closed the connection: 0 bytes read on a total of 8 expected bytes`** на старте. Безобидное, telethon просто перезаключает соединение, продолжает дальше. Если повторяется на каждом ивенте - проблема с VPN, попробовать сменить узел.

### Шаг 6. Подключение нового проекта

В корне нового проекта создать `.telegram-snapshot.json`:
```json
{
  "chats_root": "<путь вне синка>/2026-08 Клиент",
  "chats": {
    "с заказчиком": 4715985727,
    "с командой": 2644101635,
    "1-1/Иванов И.И.": 123456789,
    "бот": {"id": 4715985727, "topic_id": 30127}
  }
}
```

**`chats_root` держится вне синкаемой папки проекта.** Зеркало чата - техническое: личная переписка легко дает сотни мегабайт, которые иначе уезжают на все устройства пользователя и в облачные бэкапы (`rules/docs-maintenance.md`, "Технические артефакты в синкаемой папке"). Путь может быть абсолютным - это и есть штатный способ. Ключ не задан - скрипт берет хранилище устройства: `$TELEGRAM_SNAPSHOT_STORE` или `~/.local/share/telegram-snapshot/chats/<имя папки проекта>-<хеш пути>` (хеш нужен, чтобы два проекта с одинаковым именем папки не делили одно хранилище). Переменную задавайте **на устройстве** - в системном окружении, а не для одной команды: снапшот и дельты вычисляют путь независимо, и переменная, видимая только одному из них, разведет их по разным папкам, после чего дельты молча покажут ноль изменений. Надежнее прописать `chats_root` в конфиге явно. Единственное исключение - проект, заведенный до 23.08.2026: если папка `Встречи/чаты` в нем уже есть, она продолжает использоваться (иначе история качалась бы заново в новое место, а старая копия так и осталась бы в синке), и скрипт печатает команду переноса. Зеркало, оказавшееся внутри проекта, скрипт не запрещает, но предупреждает на каждом прогоне.

**Медиа-кэш.** Вложения всегда идут мимо проекта, в `~/.cache/telegram-snapshot/media`, и чистятся по TTL (по умолчанию 168 часов - неделя). Оба значения перекрываются: переменными `TELEGRAM_SNAPSHOT_MEDIA` / `TELEGRAM_SNAPSHOT_MEDIA_TTL` на устройстве или ключами `media_cache` / `media_ttl_hours` в проектном конфиге. Правка констант в самом скрипте не переживет `/canon` - копий на машине много.

`chat_id` для уже-экспортированного чата берется из `result.json` (поле `id` в шапке). Для чата, которого еще нет на диске, скрипт сделает **bootstrap** - создаст шапку и подтянет всю историю через Telethon (см. ниже).

**Форумные темы (topic_id).** Запись чата бывает в двух формах: короткая `"label": <chat_id>` (весь чат) и расширенная `"label": {"id": <chat_id>, "topic_id": <id>}` - для форум-чатов, когда в дельты нужна только одна тема (например, топик бота среди прочих рабочих топиков). Фильтр по `topic_id` применяется **только** в `telegram-deltas.py`; `telegram-snapshot.py` всегда тянет чат целиком (темы лежат в шапке `topics[]`, у сообщений проставлен `topic_id`) - так весь контекст чата сохраняется, а ежедневная сводка не шумит чужими темами. `topic_id` - это `id` корневого сообщения темы; узнать проще всего после первой выкачки из `result.json -> topics[]` (там `id` + `title`).

**Чат подпроекта (dest).** Необязательный ключ расширенной записи: `"Лариса CV": {"id": 464543113, "dest": "2026-03 CV Лариса/чаты/Лариса"}` - путь папки чата **от корня проекта**, вместо `chats_root/<label>`. Нужен в топологии клиент-зонтика (`rules/client-umbrella.md`): зеркало чата подпроекта живет в папке подпроекта, но чат при этом участвует в регулярном снапшоте и дельтах наравне с остальными (симметрично `dest` у mymeet-snapshot). Абсолютный путь разрешен (тот же довод, что у `chats_root` выше); относительный обязан остаться внутри проекта - выход через `..` скрипт отклоняет как опечатку; дубли целевых папок (два чата в одну, в том числе различающиеся только регистром) и папку с зеркалом другого чата он отклоняет тоже. Без `dest` поведение прежнее.

**Чат без вложений (media).** Необязательный ключ расширенной записи: `"комьюнити": {"id": 1539618878, "media": false}` - тянуть только текст и метаданные, вложения не скачивать (метаданные файлов - имя, размер, mime - в сообщениях остаются). Нужен для чатов с потоком картинок - мем-флудилки, новостные каналы: полезной нагрузки во вложениях нет, а скачивание тысяч файлов роняет прогон разрывом соединения (симптом в логе - `Server closed the connection` и `CancelledError` из `download_media`; это не "телеграм недоступен"). По умолчанию `true` - для рабочих чатов вложения нужны, и старые конфиги читаются без правок. У `telegram-pull-one.py` то же выключается флагом `--no-media`. Выключение необратимо для уже забранных сообщений: вложения скачиваются только у новых, докачки задним числом нет - вернув `media: true`, файлы пропущенного периода не получить. Окно правок и удалений (ниже) этого не меняет: в нем перечитываются текст и метаданные вложения, но сам файл не скачивается ни при `media: true`, ни при `media: false` - и ось "media" от оси "окно правок" не зависит, текстовые правки в чате без вложений подхватываются так же.

**Конвенция подпапок.** Label интерпретируется буквально как путь внутри `chats_root`: `result.json` для label `с заказчиком` лежит в `<chats_root>/с заказчиком/`, а для label `1-1/Иванов И.И.` - в `<chats_root>/1-1/Иванов И.И./`. Префикс `1-1/` имеет особый смысл - персональные чаты (личные диалоги с участниками 1-1-встреч). `telegram-deltas.py` выводит их отдельным блоком `#### 1-1` под общим списком групповых чатов, со срезанным префиксом в display name. Прочие подпапки скрипт просто проксирует как часть пути, без специальной обработки.

Сами скрипты `scripts/telegram-snapshot.py` и `scripts/telegram-deltas.py` приходят из канона при bootstrap `.AI` для типа management и при последующем `canon-sync` обновлении. Не пересобирать руками из других проектов - старые копии могут не содержать `iter_dialogs()` прогрев и режим миграции legacy формата.

### Шаг 7. Формат `result.json` и режимы скрипта

Канонический формат result.json - **расширенный** Telegram Desktop export:

```json
{
  "name": "<имя чата>",
  "type": "private_supergroup",
  "id": 2644101635,
  "topics": [
    { "id": 167, "title": "Маршруты", "date": "...", "date_unixtime": "...", "icon_color": 13338331 }
  ],
  "messages": [
    {
      "id": 168, "type": "message",
      "date": "...", "date_unixtime": "...",
      "from": "...", "from_id": "user...",
      "text": "...", "text_entities": [...],
      "topic_id": 167,
      "reply_to_message_id": 200,
      "edited": "...", "edited_unixtime": "..."
    }
  ]
}
```

Дополнения относительно сырого TG Desktop экспорта:

- `topics: []` в шапке - метаданные форумных тем (`id`, `title`, `date`, `date_unixtime`, `icon_color`). При переименовании топика обновляется `title`.
- `topic_id` у сообщения - id корневого сообщения форум-темы. Заполняется только для сообщений в форум-чатах с топиками.
- `reply_to_message_id` остается **только** для настоящих реплаев (когда пользователь ответил на конкретное сообщение внутри топика). "Цепляние к шапке топика" больше не выражается через reply - оно в `topic_id`.
- `edited` / `edited_unixtime` - если сообщение было отредактировано в TG.
- `topic_created` / `topic_edited` service-сообщения в `messages[]` **не сохраняются** - вся инфа о топиках в `topics[]`.

Скрипт `telegram-snapshot.py` работает в трех режимах:

1. **Bootstrap.** `result.json` отсутствует -> создается шапка через `resolve_entity()` (entity берется из карты диалогов `iter_dialogs`, `get_entity` - только фолбэк для чатов вне списка диалогов), история целиком подтягивается с `min_id=0`. Удобно для проектов, где нет ручного TG Desktop экспорта.
2. **Migration.** `result.json` есть, но в формате сырого TG Desktop экспорта (без `topics[]` в шапке). Скрипт:
   - сохраняет копию в `result.pre-migration.json` (бэкап перезаписывается, если миграция запускается повторно);
   - извлекает `topic_created` / `topic_edited` из `messages[]` в `topics[]`;
   - проставляет `topic_id` сообщениям, поднимаясь по цепочке `reply_to_message_id` до сообщения, чей id есть в множестве корней тем;
   - очищает `reply_to_message_id`, если он указывал ровно на корень темы (теперь это выражается через `topic_id`);
   - удаляет `topic_created` / `topic_edited` service-сообщения из `messages[]`.
   - При ошибке миграции (поломанный JSON, неполные данные) - файл не трогается, чат пропускается, бэкап остается.
3. **Incremental pull.** Стандартный режим: тянем сообщения с `id > max(messages.id, topics.id)` (high-water mark учитывает оба массива - иначе service-only хвост после последнего обычного сообщения тянулся бы каждый запуск), апдейтим `topics[]`, дописываем в `messages[]`. Если есть реальные новые сообщения/топики/правки - сохраняем `result.prev.json` как baseline до них (для `telegram-deltas.py`); если миграция случилась без новых сообщений - `result.prev.json` не трогаем, дельта будет посчитана при следующем pull с реальными новыми. Запись `result.json` и `result.prev.json` атомарная - через `*.tmp` + `os.replace`, чтобы Ctrl+C / OOM не оставили битый JSON. Отдельно от инкремента перечитывается окно последних сообщений ради правок и удалений - см. "Правки и удаления" ниже; правки и удаления сохраняют `result.json`, но baseline `result.prev.json` не двигают: дельты считаются по новым `id`.

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

**Починка старых миграций (внутри incremental pull).** На уже мигрированном снапшоте скрипт дополнительно прогоняет `repair_legacy_migration()`: достраивает `topic_id` сообщениям, потерявшим его из-за старого бага миграции (reply-on-reply внутри темы). Если что-то восстановлено - один раз сохраняет бэкап `result.pre-repair.json`. На здоровом снапшоте это no-op. Отдельным режимом не считается - срабатывает автоматически при обычном pull.

**Известные ограничения:**

- **Правки и удаления - только в окне последних 50 сообщений.** Инкремент по `id > high_water` их не видит, поэтому каждый прогон дополнительно перечитывает последние `EDIT_WINDOW` сообщений (50, переменная `TELEGRAM_SNAPSHOT_EDIT_WINDOW`): отредактированное обновляется, прежний текст уходит в `edit_history`, удаленное помечается `deleted: true` и из зеркала не стирается. Счетчики `правок: K, удалений: M` печатаются всегда, нулевые тоже - иначе "проверил, правок нет" не отличить от "не проверял" (`silent-failure.md`). Правка сообщения глубже окна в зеркало не попадет: там по-прежнему только bootstrap заново. Чат короче окна сверяется целиком, включая удаление первых сообщений. Что окно не делает: замененное вложение получает новые метаданные, но файл заново не скачивается; дельты (`telegram-deltas.py`) считаются по новым `id` - правку старого сообщения они не покажут, удаленное после baseline в "новое" не попадает. Второй запрос упал (сеть, `FloodWait`) - забранные новые сообщения все равно записываются, а в строке прогона вместо счетчиков стоит "правки не проверены": это тоже отличимое состояние, а не тишина. Прецедент, ради которого это появилось: HR, 18.08.2026 - собеседница задним числом переписала сообщение, три прогона ответили "новых нет".
- **Bootstrap не делает checkpoint.** Долгий первый pull многотысячного чата при сбое (Ctrl+C, network) начнется заново - частично сохраненного `result.json` не будет, потому что атомарная запись делается только в конце. Это компромисс простоты против устойчивости.
- **Migration по reply-цепочке имеет лимит.** `migrate_legacy` определяет `topic_id` обычного сообщения, поднимаясь от его `reply_to_message_id` до сообщения, чей id есть в множестве корней тем. Если цепочка обрывается (reply на сообщение, которого нет в экспорте - например, удалено) - `topic_id` не проставится. Контент сообщения не теряется, только связь с темой.

**Медиа-кэш вложений.** Вложения новых сообщений (фото, документы) скачиваются в локальный НЕсинкаемый кэш `~/.cache/telegram-snapshot/media/<chat_id>/`, имя файла `<msg_id>_<оригинал|расширение>`. Агент читает их сам: скрин - визуально через `Read`, файл - сканером. Ключевые свойства:

- **Кэш вне проекта - принципиально.** Папки проекта часто синкаются в облако, а вложения бывают с секретами; кэш вне синка - секреты в облако не текут.
- **`result.json` не меняется:** ни байтов, ни путей медиа туда не пишется. Корреляция сообщение<->файл по `<msg_id>` в имени файла кэша + метаданные `file_name`/`mime` в самой записи сообщения.
- **TTL 24ч:** на каждом запуске чистятся файлы кэша старше суток (по mtime). Прочитал -> сутки доступно -> подчистилось.
- Превью ссылок (webpage) вложением не считаются. Падение скачивания одного файла по обычной ошибке pull не роняет (best-effort); а вот массовый поток вложений способен уронить обработку чата целиком разрывом соединения (`CancelledError`) - для таких чатов есть `media: false` (см. "Чат без вложений").

### Шаг 8. Апгрейд скриптов в существующем проекте

Скрипты эволюционируют (новые поля, новые режимы, фиксы). В management-проекте с `.AI` обновление выполняется закрепленной процедурой через `/canon` и `canon-sync`; она проверяет конфликты и сохраняет проектные данные. Для legacy-only клиента переход в `.AI` сначала нужно отдельно запросить через migrate. Не копируй скрипты вручную из других проектов - они могут расходиться по версии.

## Несколько аккаунтов

Если переписка ведется с двух номеров (рабочий и, например, отдельный для откликов), контур работает с обоими: аккаунты описываются в `auth.json`, а каждый чат помечается своим.

**1. Аккаунты в `auth.json`** - секция `accounts`, имя аккаунта -> его настройки:

```json
{
  "api_id": 17349,
  "api_hash": "344583e45741c457fe1862106095a5eb",
  "accounts": {
    "default": {},
    "cv": {}
  }
}
```

Ключи верхнего уровня (`api_id`, `api_hash`, `proxy`) наследуются каждым аккаунтом - одно приложение на несколько номеров - и при необходимости переопределяются внутри аккаунта.

**`session_name` - исключение: он НЕ наследуется.** По умолчанию он равен имени аккаунта, поэтому у каждого свой `<имя>.session`. Это важно при миграции: если в старом плоском конфиге был задан `session_name`, а сверху добавили `accounts`, наследование посадило бы все аккаунты на один session-файл - то есть на одну авторизованную сессию, и никакой изоляции бы не было. Задать аккаунту свое имя сессии можно явно, внутри его секции; если два аккаунта укажут одно и то же, скрипт откажется работать и скажет, какие именно столкнулись.

Старый плоский формат (поля в корне, без `accounts`) продолжает читаться как единственный аккаунт `default` - существующие проекты менять не нужно.

**2. Аккаунт у чата** в `.telegram-snapshot.json` - расширенная запись с полем `account` (без него подразумевается `default`):

```json
{
  "chats": {
    "с командой": 4715985727,
    "рекрутер X": {"id": 123456789, "account": "cv"}
  }
}
```

Snapshot группирует чаты по аккаунту и обходит аккаунты последовательно, каждый своей сессией.

**3. Первый логин нового аккаунта - только через snapshot.** `telegram-snapshot.py` использует интерактивный `client.start()` (спросит номер и код), а `telegram-send.py` намеренно неинтерактивен - он лишь проверит авторизацию и откажет. Поэтому порядок: добавить аккаунт в `auth.json`, пометить им чат, один раз прогнать snapshot и залогиниться - после этого работает и отправка.

**4. Драйверы по id** берут аккаунт флагом: `telegram-pull-one.py <id> <путь> --account cv`, `telegram-send-one.py <id> --account cv --text "..."`.

**Типовая ошибка.** Чат не резолвится ("не удалось найти чат") или резолвится в чужой - первым делом проверь поле `account` у этого чата: чат виден только той сессии, которой принадлежит, и карта диалогов у каждого аккаунта своя.

`telegram-deltas.py` про аккаунты не знает и знать не должен - он работает с уже выкачанным JSON, сессия ему не нужна.

## Single-chat-by-id: pull одного чата вне конфига

Для сценариев, где чат в общий конфиг класть нельзя (разовый чат, клиент трека поддержки - не должен попадать в ежедневные pull/дельты), есть драйвер `scripts/telegram-pull-one.py`: инкрементальный pull ОДНОГО чата по id в произвольную папку, минуя `.telegram-snapshot.json`:

```bash
python3 scripts/telegram-pull-one.py <chat_id> <out_path> [expected_username]
```

`out_path` - абсолютный путь (штатно - в хранилище зеркал вне синка) либо путь от корня проекта; во втором случае скрипт предупреждает до выкачки, что зеркало окажется в синкаемой папке. Результат в `<out_path>/result.json` (тот же формат, тот же инкремент, тот же медиа-кэш - вся логика переиспользуется из `telegram-snapshot.py` через импорт). Опциональный `expected_username` - страховка: если username чата не совпал или отсутствует, pull прерывается с кодом 2 до выкачки, чтобы не выкачать чужой чат. Явно переданный пустой `expected_username` - ошибка ввода с кодом 2 до создания клиента. Код совпадает с отказом по username в `telegram-send-one.py`. Симметричная отправка по id - `scripts/telegram-send-one.py` (см. скилл `telegram-send`).

## Запуск с сервера / прокси

С хостов, где прямой доступ к Telegram API режется DPI (типично для RU-датацентров), telethon на каждом коннекте падает с `Attempt N at connecting failed: TimeoutError`. Env-прокси (`HTTP_PROXY`/`ALL_PROXY`) telethon не читает - прокси задается полем `proxy` в `~/.config/telegram-snapshot/auth.json` этого устройства:

```json
{ "api_id": ..., "api_hash": "...", "session_name": "...", "proxy": "socks5://127.0.0.1:7890" }
```

- Формат - `scheme://host:port`. Для socks-схем нужен пакет: `pip3 install --user "python-socks[asyncio]"`. Нет поля - прямое подключение, поведение прежнее.
- Настройка per-device: ноутбук ходит напрямую (без поля), серверная VM - через свой локальный прокси.
- На сервере - **отдельное `session_name`** и свой интерактивный первый запуск: одновременная работа одного `.session` с двух IP кончается `AuthKeyUnregisteredError` / конфликтом auth-ключа.
- Поле читают все клиентские скрипты: `telegram-snapshot.py`, `telegram-pull-one.py`, `telegram-send-one.py`, `telegram-send.py` (хелпер `client_kwargs`).

## Общая сессия: `database is locked`

Авторизация одна на устройство (`auth.json` + `.session`), а `.session` - это sqlite. Пока с ней работает один процесс, остальные получают изнутри telethon:

```
sqlite3.OperationalError: database is locked
```

Типичный источник - **параллельная сессия Claude из другого проекта**, запустившая свой `telegram-snapshot.py`: сессия у них общая с нашей. Падает любой клиентский скрипт, обычно на подключении (`client.connect()` / `client.start()` -> `sessions/sqlite.py`).

**Это не поломка и не зависший процесс.** Чужой прогон дорабатывает сам и отпускает сессию - как правило в пределах 30-60 секунд.

Скрипты закрывают **подключение** сами: оно идет через `connect_with_retry` (5 попыток с паузой 15 секунд, повтор только на `database is locked`; sqlite ждет еще до 5 секунд внутри каждой попытки и столько же в закрытии между попытками, то есть на живом локе до отказа проходит около двух минут). Закрытие клиента тоже безопасно - `disconnect_quietly` не роняет прогон своей ошибкой. Не покрыто ретраем открытие сессии в конструкторе `TelegramClient` (редкий случай: лок ровно на чтении схемы) - там ошибка прилетит сразу, без ожидания.

**Лок на disconnect - не провал.** Строка `disconnect не отработал (OperationalError: database is locked)` сама по себе означает лишь, что не закрылось соединение, - судить по ней об исходе прогона нельзя ни в какую сторону (она бывает и в неудачной попытке подключения). Смотреть надо на итог.

Отсюда правило для агентских оберток и крон-промптов: **не считать попытку неудачной по одному совпадению строки "database is locked" в выводе.** Признак успеха прогона - итоговая строка `OK:` (`OK: +N сообщений` при заборе, `OK: отправлено` при отправке) и нулевой код возврата: `OK:` означает, что прогон штатно завершен - данные забраны (и записаны, если было что писать; `+0` без новых сообщений - тоже штатный исход), а при отправке сообщение принято сервером, - что бы ни писалось про lock выше. Обертка вида `grep -qi "database is locked" && retry` без этой проверки прячет свежие сообщения (скрипт их уже записал, а обертка доложила "проверить не удалось") - на отправке тот же паттерн еще и провоцирует дубль сообщения живому получателю. Два уточнения:

- При частичном отказе (упал хотя бы один чат) снапшот печатает не `OK:`, а `ЧАСТИЧНО: +N сообщений; провалено чатов: K (<labels>)` и возвращает код 1; упавшие чаты видны и построчно (`!! <label>`, "чат пропущен"). Успешные чаты при этом уже записаны - обертка ретраит только упавшие, а не весь прогон.
- Для отправки правило действует, только когда `OK: отправлено` напечатан. Лок, прилетевший ДО него (в кадре `send_message`/`send_file`), - отдельный случай "статус неизвестен, повтор опасен": см. скилл `telegram-send`, раздел про `database is locked`.

Что делать, когда ошибка все же дошла:

- **Чужой процесс не убивать.** `pgrep -af "telegram-(snapshot|pull-one|send)"` уместен только чтобы убедиться, что это чужая штатная работа. `kill`/`pkill` по нему рвет выкачку другого проекта на середине.
- **Не "лечить" сессию.** Удаление `.session-journal` или самого `.session` - это сброс авторизации устройства, а не исправление блокировки.
- **Не эскалировать в починку.** Отложить прогон (следующий тик расписания, позже по просьбе пользователя) и сказать пользователю, что сессию держит другой проект. Ручной повторный запуск через минуту-другую - тоже нормально.
- **Повтор снапшота безопасен, повтор отправки - нет.** Pull инкрементальный и идемпотентный. Отправка - необратимое внешнее действие, там свои правила (см. скилл `telegram-send`).
- **В мульти-аккаунтном прогоне лок обрывает весь снапшот**, а не только свой аккаунт: аккаунты обходятся последовательно, и необработанные останутся до следующего запуска (в отличие от ошибки по одному чату - та печатается и не мешает остальным).
- **Ретрай ставится только на подключение.** Если добавляешь новую точку подключения - через `connect_with_retry`. Оборачивать в ретрай саму отправку нельзя: повтор после успешного `send_message` даст получателю дубль (см. скилл `telegram-send`).

## Жесткие правила

1. **Auth-файл и .session - НЕ кладем в проектную папку.** Если проектная папка синхронизируется через облако (Yandex.Disk, Dropbox, iCloud), утечка api_hash и сессии в облако недопустима. Только в `~/.config/telegram-snapshot/` (локально на каждом устройстве отдельно).
2. **Первый интерактивный запуск делает пользователь сам в терминале.** Bash из этого скилла на интерактивный ввод не способен. Подготовить auth.json, дать команду, дождаться отчета пользователя.
3. **Не запускать `iter_messages` напрямую без get_dialogs() прогрева на свежей сессии** - получишь PeerUser-ошибку. Прогрев один раз на старте `amain()`.
4. **При использовании публичных ключей Telegram Desktop** - помнить, что это серая зона по ToS. Сами числа `api_id`/`api_hash` не секрет (они в открытых исходниках tdesktop), но ответственность за использование лежит на пользователе. Ограничения: не использовать для коммерческой автоматизации; не выходить за объем активности живого пользователя (без массовых запросов, рассылок, бот-подобных паттернов); не подключать к публичным сервисам/ботам. Только некоммерческие персональные сценарии (свой архив чатов, дейлики).
5. **Зеркало пишется вне синкаемой папки сразу, а не исключается после.** История публичного канала или многолетней переписки это десятки мегабайт `result.json`, и синхронизация непрерывна: между созданием файла и любым следующим действием он уже на всех устройствах, а исключение задним числом этого не отменяет. Отсюда `chats_root` вне проекта (шаг 6). Механика и порядок уборки уже уехавшего - `rules/docs-maintenance.md`, раздел "Технические артефакты в синкаемой папке".

## Связанные файлы

- `scripts/telegram-snapshot.py` в корне проекта - эталонный скрипт инкрементального pull с `iter_dialogs()` прогревом и форматом записи, совместимым с Telegram Desktop export.
- `scripts/telegram-deltas.py` в корне проекта - расчет дельт между текущим и предыдущим snapshot для блока "Новое в чатах" в дейликах.
- `scripts/telegram-send.py` в корне проекта - отправка сообщения в чат от имени пользователя (переиспользует ту же авторизацию и `.telegram-snapshot.json`). Настройка отдельно не нужна - если snapshot работает, работает и отправка. См. скилл `telegram-send`.
- `scripts/telegram-pull-one.py` - драйвер single-chat-by-id: pull одного чата по id в произвольную папку, минуя конфиг (см. одноименную секцию).
- `~/.cache/telegram-snapshot/media/<chat_id>/` - локальный несинкаемый кэш вложений (TTL 24ч, см. "Медиа-кэш вложений").
- `~/.config/telegram-snapshot/auth.json` - локальные credentials на устройство (не в проекте).
- `.telegram-snapshot.json` в корне проекта - проектная карта `{label: chat_id}` (или `{label: {id, topic_id}}` для фильтрации форум-чата по теме).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.