aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 29c75211f222e6147c9a99350771680a1f1f445b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# tg_backup

Incremental media backup for Telegram chats. Downloads photos and documents from a chat, remembers where it stopped, and picks up from there on the next run.

Single Python file, one dependency ([Telethon](https://docs.telethon.dev/)).

## Install

```bash
pip install telethon
cp tg_backup.py ~/bin/ && chmod +x ~/bin/tg_backup.py
```

## Setup

Run it once with no arguments to create the config:

```bash
tg_backup.py
```

It writes `~/.config/telegram_backup/config.json` and exits. Fill in the two values from [my.telegram.org](https://my.telegram.org) (API development tools):

```json
{
    "api_id": 12345,
    "api_hash": "<your 32-character hash>"
}
```

`api_id` is an integer, `api_hash` a 32-character hex string.

Nothing else from that page is needed. FCM credentials and MTProto server addresses are for full client implementations.

The first real run prompts for your phone number and login code. That session is saved and shared by every subsequent run, so you log in once.

## Usage

List your chats to find a target:

```bash
tg_backup.py --list-chats
```

```
             ID  TYPE      USERNAME              NAME
 -1001234567890  group     @examplegroup         Example Group
      123456789  user      @examplecontact       Example Contact
      987654321  user      -                     Contact Without Handle
```

Back one up:

```bash
tg_backup.py --target=@examplegroup --archive-dir ~/backups/examplegroup
tg_backup.py --target=-1001234567890 --archive-dir ~/backups/examplegroup
```

Numeric IDs are more durable than usernames, which their owners can change. Chats showing `-` have no public handle and can only be addressed by ID.

`--target` also accepts `me` (Saved Messages), t.me links, and invite links for groups you have already joined.

Update it later with just the directory:

```bash
tg_backup.py --archive-dir ~/backups/examplegroup
```

The target is recorded in the archive directory on the first run, so repeat runs do not need it. Passing the same `--target` again is fine; the `@` and letter case do not have to match.

Passing a *different* one is refused, because the directory's resume position belongs to the old chat and would skip the new chat's history. Use one directory per chat. If a chat genuinely changed handle or ID, `--force-target` accepts the new target and keeps the resume position. Switching between a chat's `@username` and its numeric ID also trips this, as the two cannot be compared without resolving them first.

### Options

| Option | Default | Meaning |
|---|---|---|
| `--target` | first run only | Chat to back up: `@username`, numeric ID, t.me link, or `me`. Reused from the archive directory afterwards |
| `--archive-dir` | `./tg_archive` | Where media and resume state are written; created if missing |
| `--list-chats` | | Print your dialogs with IDs and usernames, then exit |
| `--force-target` | | Accept a `--target` that differs from the one saved in the archive directory |
| `--self-check` | | Run internal assertions and exit. No network, no login |

Use one archive directory per chat. Prefer absolute paths: the default is relative to the current directory, so running from elsewhere silently starts a second archive.

### Negative IDs need `=`

```bash
tg_backup.py --target=-1001234567890     # correct
tg_backup.py --target -1001234567890     # error: expected one argument
```

Without `=`, argparse reads the leading `-` as the start of another flag.

## How it works

Messages are processed oldest to newest, and the last message ID seen is written to `state.json` in the archive directory after every message, alongside the chat it belongs to. An interrupted run resumes from there instead of rescanning the whole chat.

Media is saved as `{message_id}_{original_name}`. Downloads land in a temporary `.part` file and are renamed only on success, so a killed run never leaves a truncated file that a later run would mistake for a complete one. Files that already exist are skipped, so even a lost `state.json` degrades to a slow rescan rather than re-downloading everything.

Rate limits are handled at two levels: Telegram's requested wait is honoured for both individual downloads and history fetches, and other errors (dropped connections, expired file references) are retried with exponential backoff. A download that fails permanently is skipped rather than stalling the run forever, and its message ID is appended to `failures.json` so the gap is auditable:

```bash
jq -s 'map(.id)' ~/backups/examplegroup/failures.json
```

One JSON object per line, written only after all retries are exhausted. Nothing retries or prunes these; the file is a record, not a queue.

Only media is saved. Message text and captions are not.

## Files

| Path | Contents |
|---|---|
| `~/.config/telegram_backup/config.json` | API credentials |
| `~/.config/telegram_backup/session.session` | Login session, shared across all archive directories |
| `<archive-dir>/state.json` | Resume position and saved target for that chat |
| `<archive-dir>/failures.json` | Message IDs whose media never downloaded |
| `<archive-dir>/*` | Downloaded media |

The session file is account credentials: anyone holding it can read your Telegram. It is created mode `0600` inside a `0700` directory. Keep it out of version control and off shared storage.

## License

Copyright (C) 2026 Danilo M. <danix@danix.xyz>

Released under the GNU General Public License version 2. See [LICENSE](LICENSE) for the full text.

## Development Approach

This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.

All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.

The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.