Настроить или починить инкрементальный 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
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.
[](https://www.skillsdirectory.com/skills/dewil-telegram-snapshot)
---
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}}` для фильтрации форум-чата по теме).