diff options
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 46 |
1 files changed, 46 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..551fc5f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,46 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Single-file Python script (`tg_backup.py`, ~115 lines) that incrementally downloads media (photos + documents) from a Telegram chat/group via Telethon. No package, no tests, no build step. Keep it single-file unless there is a real reason not to. + +Dependency: `telethon` only. + +## Running + +```bash +./tg_backup.py --list-chats --archive-dir ./tg_archive # find IDs +./tg_backup.py --target @somegroup --archive-dir ./tg_archive +./tg_backup.py --target=-1001234567890 --archive-dir ./tg_archive +``` + +Negative chat IDs need `--target=<id>`; argparse reads a bare `-100...` as a flag. `--list-chats` prints ID, type, `@username` (or `-` when the chat has no public handle), and display name. + +`parse_target` converts digit-only targets to `int` before `get_entity`. This is load-bearing: Telethon resolves a numeric *string* as a username and fails, while an `int` hits the session's entity cache. Usernames, `me`, and t.me links pass through as strings. + +First run without a config writes a template to `~/.config/telegram_backup/config.json` and exits; the user fills in `api_id`/`api_hash` from my.telegram.org. Telethon then prompts interactively for phone/code on first auth, so the script cannot be run unattended the first time. + +## Design invariants + +Three things are load-bearing and easy to break: + +1. **`reverse=True` on `iter_messages`** (`tg_backup.py:75`). Processing must be oldest-to-newest. `state['last_id']` is a high-water mark used as `min_id` on resume; iterating newest-first would make it skip unfetched history permanently. +2. **State saved after every message**, not at the end, via temp-file + `os.replace`. Cheap insurance against interruption; do not "optimize" into a batched write without keeping resume correctness, and keep the write atomic. A truncated `state.json` makes the next run unstartable. +3. **Two-level retry handling.** The inner `download_with_retry` retries a single file up to `MAX_RETRIES`: FloodWait sleeps the server-requested interval, other exceptions (network drops, expired file references, disk errors) get exponential backoff so one flaky socket can't kill an overnight run. The outer `while True` in `run_backup` catches FloodWait on history fetching, saves state, sleeps, and restarts the iterator from the saved id. A permanently failed download still advances state on purpose, so one broken file can't wedge the backup forever. +4. **Media downloads to `{name}.part`, renamed on success.** The dedup check is `filepath.exists()`, so a truncated file at the final path would be skipped forever as if complete. The `.part` file is unlinked on failure and on interrupt, leaving the message to be retried on the next run. + +Resume/dedup is also file-based: media is named `{message.id}_{sanitized_name}` (see `safe_filename`, which strips sender-controlled path components) and an existing path is skipped, so state loss degrades to a slow rescan rather than re-downloading everything. + +## Checks + +`python3 tg_backup.py --self-check` exercises `safe_filename` path-traversal handling and `save_state`/`load_state` round-tripping. No network, no login. It does import telethon, so it needs the dep installed. + +## Layout + +`--archive-dir` holds media files and `state.json`, and is created if missing (`parents=True`). It is per-chat: one dir per backed-up chat. + +Auth lives in `~/.config/telegram_backup/` (mode 0700): `config.json` with the api_id/api_hash, and `session.session`, a single Telethon session **shared by every archive dir**, so one login covers all chats. `migrate_session` relocates a pre-existing `<archive_dir>/session.session` to the shared path on first run rather than forcing a re-login; it is a no-op once the shared session exists. It uses `shutil.move`, not `os.replace`, because the two paths can be on different filesystems. + +The session file is account credentials: anyone holding it can read the account's Telegram. Keep it 0600 and out of git. |
