telstore 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,78 +1,68 @@
1
1
  # telstore
2
2
 
3
- Split large files into 1.8GB chunks, store them on Telegram, and restore them intact.
3
+ Split large files into chunks, store them on Telegram, and restore them byte-for-byte.
4
+
5
+ telstore signs in with your own Telegram account over MTProto, not a bot: the Bot API caps
6
+ uploads at 50MB per file, a user account gets 2GB.
7
+
8
+ Needs Node.js 18+ and an `api_id`/`api_hash` from <https://my.telegram.org> → API
9
+ development tools; `login` asks for those, your phone number and the verification code.
4
10
 
5
11
  ## Quick start
6
12
 
7
13
  ```bash
8
- npx telstore login # once only
9
- npx telstore config chat @my_backups # where backups go, from now on
10
- npx telstore data.tar # split it and send it there
11
- npx telstore data.tar --to @somewhere # somewhere else, this run only
12
- npx telstore config # every setting and where its value comes from
13
- npx telstore status # account, destination, unfinished backups
14
- npx telstore list # what is already stored in the destination
14
+ npx telstore login # once only
15
+ npx telstore config chat @my_backups # where backups go, from now on
16
+ npx telstore data.tar # split it and send it there
17
+ npx telstore list # what is already in the destination
15
18
  npx telstore restore telstore-20260905-7f3a91
16
- npx telstore delete telstore-20260905-7f3a91 # take it back out of the chat, for good
17
- npx telstore token # a session token for a machine you do not trust
18
19
  ```
19
20
 
20
- ## What you need
21
-
22
- - Node.js 18 or newer.
23
- - An `api_id` and `api_hash` from <https://my.telegram.org> → API development tools. The `login` command asks for both, plus your phone number and the verification code.
21
+ ## Commands
24
22
 
25
- telstore signs in with your own Telegram account (MTProto), not a bot. That is a hard requirement: the Bot API caps uploads at 50MB per file, while a user account gets 2GB.
23
+ | Command | What it does |
24
+ |---|---|
25
+ | `telstore login` | Log in to Telegram. Add `--token` to log in with a session token instead. |
26
+ | `telstore <file>` | Split the file and upload it. Prints the `backupId` you restore with. |
27
+ | `telstore list` | The backups stored in the destination, newest first. |
28
+ | `telstore restore <backup-id>` | Download every chunk and reassemble the file. |
29
+ | `telstore delete <backup-id>` | Remove a backup's chunks and manifest from the chat, for good. |
30
+ | `telstore status` | Account, destination, and unfinished backups. |
31
+ | `telstore config` | Show or change settings. |
32
+ | `telstore token` | Print a session token for a machine you do not trust. |
33
+ | `telstore logout` | Remove the locally stored session. |
26
34
 
27
- ## Settings and flags
35
+ ## Settings
28
36
 
29
- There are two ways to say what telstore should do, and they never overlap. **`config` writes;
30
- flags do not.** A flag applies to the run you typed it on and changes nothing on disk, so
31
- `--to @elsewhere` sends one backup elsewhere without moving the destination for the next one.
37
+ `config` writes; flags do not. A flag applies to the run you typed it on and changes nothing
38
+ on disk, so `--to @elsewhere` sends one backup elsewhere without moving the destination for
39
+ the next one.
32
40
 
33
41
  ```bash
34
- npx telstore config # everything, and whether it is yours or a default
35
- npx telstore config chat # one value, bare, ready to pipe
36
- npx telstore config chunkSize 500MB # change it for good
37
- npx telstore config chunkSize --unset # back to the default
42
+ npx telstore config # every setting, and whether it is yours or a default
43
+ npx telstore config chunkSize 500MB # change it for good
44
+ npx telstore config chunkSize --unset # back to the default
38
45
  ```
39
46
 
40
47
  | Setting | Flag | Default | Meaning |
41
48
  |---|---|---|---|
42
- | `chat` | `--to` | none | `@username`, `-100123…`, or `me`. A negative channel id works with a space or an `=`, as a flag or as a config value. |
43
- | `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`. Hard ceiling 1950MB. An unfinished backup keeps the size it started with. |
44
- | `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts sent in parallel while uploading. An integer from 1 to 64. |
45
- | `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices fetched in parallel while restoring. An integer from 1 to 64. |
46
- | `limit` | `--limit` | `20` | How many backups `list` shows, newest first |
47
- | `verbose` | `--verbose` | off | Show the Telegram client's own connection logs, hidden by default so they do not break up the progress bar |
48
-
49
- `--yes` has no setting either — it answers the confirmation `delete` asks before destroying
50
- a backup, and an answer stored in a file would be an answer to a question nobody heard.
51
-
52
- `--out <path>` has no setting: it names where one particular restore should write, and
53
- defaults to the basename in the manifest. Relative paths resolve against the current directory.
54
-
55
- `config` reads and writes only settings — the `api_id`, `api_hash` and session that `login`
56
- stores in the same file are not reachable from it. A value is checked before it is written,
57
- so `config chunkSize 9GB` is refused there and then rather than at the start of a long upload.
49
+ | `chat` | `--to` | none | `@username`, `-100123…`, or `me` |
50
+ | `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`. Ceiling 1950MB. |
51
+ | `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts in parallel while uploading, 1–64 |
52
+ | `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices in parallel while restoring, 1–64 |
53
+ | `limit` | `--limit` | `20` | How many backups `list` shows |
54
+ | `verbose` | `--verbose` | off | Show the Telegram client's own connection logs |
55
+
56
+ Three flags have no setting behind them: `--out <path>` names where one restore writes,
57
+ `--yes` skips the confirmation `delete` asks, and `--token` takes no value — a token written
58
+ on the command line would sit in `ps` and in that machine's shell history, so it is pasted at
59
+ a prompt that does not echo.
58
60
 
59
61
  ## What the chat looks like
60
62
 
61
- Every chunk goes up as a document captioned `📦 <backupId> · 3/12`, and the manifest that
62
- follows carries a summary card:
63
-
64
- ```
65
- 🗄 data.tar
66
- 💾 21.4 GB · 12 chunks
67
- 🆔 telstore-20260905-7f3a91
68
- 📅 2026-09-05 16:40 UTC
69
-
70
- ↩ npx telstore restore telstore-20260905-7f3a91
71
- #telstore
72
- ```
73
-
74
- `npx telstore list` reads those cards straight out of the chat — one search, no downloads —
75
- and lays them out as a table:
63
+ Every chunk goes up as a document captioned `📦 <backupId> · 3/12`, followed by a manifest
64
+ carrying a summary card — file name, size, id, date and the restore command. `list` reads
65
+ those cards straight out of the chat, one search and no downloads:
76
66
 
77
67
  ```
78
68
  Destination https://web.telegram.org/k/#@my_backups
@@ -84,141 +74,50 @@ telstore-20260901-9de447 photos.zip 940.3 MB 1 2026-09-01
84
74
  2 backups. Restore with: npx telstore restore <backup-id>
85
75
  ```
86
76
 
87
- A backup uploaded before the card existed still gets a row, with dashes where the caption
88
- says nothing — `list` reports what the chat holds and never fills gaps with guesses.
89
-
90
- ## How it works
91
-
92
- Every run mints a `backupId`. The file is read directly by offset — no temporary copies — and uploaded as documents named `<backupId>.partNNNN`. Once every chunk is up, telstore sends a JSON manifest listing the message id and sha256 of each one. Restore needs only the `backupId`: it finds the manifest in the chat, downloads each chunk to its exact position in a `.partial` file, checks every chunk's sha256 and size, and renames it to the real file only after *all* chunks match.
93
-
94
- If the connection drops during an **upload**, just run the same command again — progress lives in `~/.telstore/state/` and finished chunks are skipped, keeping the same `backupId`. Two things to know about rerunning:
95
-
96
- - Running again against a destination that differs from the one in the unfinished progress makes telstore **refuse to run** rather than silently redirect — one backup cannot be split across two destinations. The error names the chat to pass as `--to` to carry on, or the state file to delete to start a new backup. It reads the same whether the mismatch came from a flag or from your configured `chat`.
97
- - Running again **without** `--chunk-size` resumes at the size the backup started with, whatever your configured `chunkSize` says today. A setting is what to use when nobody asks for anything; it is not somebody asking.
98
- - Running again **with** a `--chunk-size` that differs from that size makes telstore **refuse to run**: the chunks already in the chat were cut that way and cannot be re-cut. Drop the flag to carry on, or delete the state file to start a new backup — which leaves the chunks already sent in the chat with nothing pointing at them.
99
-
100
- `Ctrl-C` during an upload names the backup it was working on, so `telstore status` and a later `restore` have something to go on. telstore keeps the **20 most recent** unfinished backups in `~/.telstore/state/`; starting a new one past that drops the oldest record and says which id it dropped. Only the local record goes — the chunks that backup sent stay in the chat, searchable by that id, but it can no longer be resumed.
101
-
102
- **Restore keeps no state to resume from.** Pressing `Ctrl-C` mid-restore saves nothing — running again starts over.
103
-
104
- ## Deleting a backup
105
-
106
- ```bash
107
- npx telstore delete telstore-20260905-7f3a91
108
- ```
109
-
110
- It prints what it is about to destroy, asks once, and then removes every chunk message and
111
- the manifest from the chat and drops the local record if there is one. `--yes` skips the
112
- question. **There is no undo** — Telegram is the only copy.
113
-
114
- It also works on a backup that never finished: those have chunks in the chat but no manifest,
115
- so `list` cannot see them and only `status` knows they exist. `delete` reads the local record
116
- instead and clears both.
77
+ ## Resuming an upload
117
78
 
118
- The chunks go first and the manifest goes last, deliberately. The manifest is the only list
119
- of the message ids, so if a delete is interrupted — Ctrl-C, a dropped connection — running
120
- the same command again finds that list still there and finishes the job. Telegram says
121
- nothing about an id that is already gone, so a second run costs nothing. In between the two
122
- runs the backup still shows up in `list`, and a `restore` of it fails loudly rather than
123
- handing over a partial file.
79
+ Progress lives in `~/.telstore/state/` (the 20 most recent), so running the same command
80
+ again skips the finished chunks and keeps the same `backupId`. A resumed backup keeps the
81
+ chunk size it started with; passing a `--chunk-size` or a destination that differs from its
82
+ own makes telstore refuse to run rather than re-cut or redirect it silently.
124
83
 
125
- Two things it refuses rather than guesses at: a manifest whose body names a *different*
126
- backup (a file renamed in the chat — its message ids point at somebody else's chunks), and a
127
- manifest or local record giving a message id that is not a whole positive number. Neither
128
- deletes anything at all. A manifest too damaged for `restore` to use *can* still be deleted —
129
- that is usually the one you want gone.
84
+ **Restore keeps no state** — `Ctrl-C` mid-restore saves nothing, running again starts over.
85
+ And `delete` has **no undo**: Telegram is the only copy.
130
86
 
131
87
  ## Running on a machine you do not trust
132
88
 
133
- `login` leaves your Telegram session on the machine you run it on, in plain text. When that
134
- machine is not yours, print a **session token** on one that is and log in with that instead:
89
+ `login` leaves your session on disk in plain text. When the machine is not yours, print a
90
+ **session token** on one that is and log in with that instead:
135
91
 
136
92
  ```bash
137
- # on the machine you are already logged in on
138
- npx telstore token # asks for a passphrase, twice
139
- # prints one line: tls1.…
140
-
141
- # on the other machine
142
- npx telstore login --token # paste the token, then type the passphrase
143
- npx telstore restore telstore-20260905-7f3a91
144
- npx telstore logout # when you are done
93
+ npx telstore token # on your own machine: asks for a passphrase, prints one line
94
+ npx telstore login --token # on the other one: paste the token, then the passphrase
95
+ npx telstore logout # when you are done
145
96
  ```
146
97
 
147
98
  The token holds your `api_id`, `api_hash`, session and settings, encrypted with the
148
- passphrase (AES-256-GCM, key derived with scrypt). It is one line of base64url, so it
149
- survives a chat message, an email or a QR code — and a copy that leaks without the
150
- passphrase is not enough to use your account.
151
-
152
- `login --token` stores the token exactly as it arrived, so **the session never exists on that
153
- machine in a form anyone can read**. Every command that talks to Telegram asks for the
154
- passphrase and opens it in memory:
155
-
156
- ```json
157
- { "sealed": "tls1.…", "settings": { "chat": "@my_backups" } }
158
- ```
159
-
160
- `npx telstore status` shows which of the two you are running:
161
-
162
- ```
163
- Session /home/you/.telstore/config.json (sealed — opened with a passphrase)
164
- Account Sho (@shovity)
165
- Destination https://web.telegram.org/k/#@my_backups
166
- Unfinished none
167
- ```
99
+ passphrase (AES-256-GCM, scrypt), as one line of base64url that survives a chat message or a
100
+ QR code. `login --token` stores it exactly as it arrived and every command asks for the
101
+ passphrase, so the session never exists on that machine in readable form.
168
102
 
169
- ### What is left behind
170
-
171
- - **No readable session.** `logout` removes the sealed blob, and with it the `api_hash`,
172
- which lives inside it.
173
- - **State files, yes.** An upload writes `~/.telstore/state/<key>.json` so it can be resumed
174
- after an interruption. It holds the file path, the chunk sizes and the message ids — no
175
- secret — and losing it would strand chunks in the chat with nothing able to name them.
176
-
177
- ### Things worth knowing before you use one
178
-
179
- - **`--token` takes no value, on purpose.** A token written on the command line would sit in
180
- `ps` for the whole life of the command and stay in that machine's shell history afterwards.
181
- It is pasted at a prompt that does not echo it.
182
- - **The passphrase may be left empty**, and then the token says so on its face: it starts
183
- `tls0.` instead of `tls1.`, `token` warns, and `login` stores the session in plain text
184
- exactly as an ordinary login would. telstore will not write a file that looks encrypted and
185
- is not.
186
- - **telstore cannot take a token back.** There is no expiry and no revocation. Like `logout`,
187
- it says nothing to Telegram — to end the session for good, open Telegram → Settings →
188
- Devices (Active sessions) and terminate it. Every copy of the token dies with it.
189
- - **A wrong passphrase and a damaged token are the same event** to AES-GCM: one failed
190
- authentication check. telstore names both possibilities rather than guessing which it was.
103
+ There is no expiry and no revocation: to end a session for good, terminate it under Telegram
104
+ → Settings → Devices. An empty passphrase is allowed, and then the token says so on its face
105
+ — it starts `tls0.` and `login` stores the session in plain text.
191
106
 
192
107
  ## Limits worth knowing
193
108
 
194
- - A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file, an arithmetic ceiling of about 1953MB, and telstore stops at 1950MB to leave a safety margin.
195
- - The data is **not** encrypted. Don't upload anything you would mind sitting on someone else's infrastructure. The one thing telstore does encrypt is a session token, and that protects your login rather than your files.
196
- - Deleting a chunk message on Telegram destroys the backup, with no way to recover it. Use `npx telstore delete <backup-id>` when that is what you actually want.
197
- - Keep the `backupId`. Without it you have to hunt for the manifest in the chat by hand.
198
-
199
- ## Where the config lives
200
-
201
- `~/.telstore/config.json` (mode 600) holds `apiId`, `apiHash` and the session at the top level, with everything `config` manages under `settings`:
202
-
203
- ```json
204
- {
205
- "apiId": 123456,
206
- "apiHash": "…",
207
- "session": "…",
208
- "settings": { "chat": "@my_backups", "chunkSize": 524288000 }
209
- }
210
- ```
211
-
212
- A machine logged in with `npx telstore login --token` holds `sealed` instead of those three
213
- fields, and nothing else about the account:
214
-
215
- ```json
216
- { "sealed": "tls1.…", "settings": { "chat": "@my_backups" } }
217
- ```
109
+ - **Your data is not encrypted.** Don't upload anything you would mind sitting on someone
110
+ else's infrastructure — the one thing telstore encrypts is a session token, and that
111
+ protects your login rather than your files.
112
+ - A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file.
113
+ - Deleting a chunk message in the Telegram app destroys the backup, and keeping the
114
+ `backupId` is what saves you hunting for its manifest in the chat by hand.
218
115
 
219
- A file holding both shapes is refused rather than guessed at — there would be no way to know
220
- which account was meant.
116
+ Settings and credentials live in `~/.telstore/config.json`, mode 600 — `apiId`, `apiHash` and
117
+ the session at the top level (or a single `sealed` blob after `login --token`), everything
118
+ `config` manages under `settings`. Editing it by hand is fine: a value that cannot be used is
119
+ named on the next run, with the file and the key.
221
120
 
222
- Editing it by hand is fine, and a value that cannot be used is named on the next run — with the file and the key, never with a flag you did not type.
121
+ ## License
223
122
 
224
- `npx telstore logout` **only deletes the locally stored session** and keeps the rest — the session is still alive on Telegram's side. To revoke access for good, open Telegram → Settings → Devices (Active sessions) and terminate that session.
123
+ MIT
package/bin/telstore.js CHANGED
@@ -1,14 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { route, HELP, interruptMessage } from '../src/cli.js'
3
- import { runLogin } from '../src/commands/login.js'
4
- import { runConfig } from '../src/commands/config.js'
5
- import { runDelete } from '../src/commands/delete.js'
6
- import { runList } from '../src/commands/list.js'
7
- import { runLogout } from '../src/commands/logout.js'
8
- import { runRestore } from '../src/commands/restore.js'
9
- import { runStatus } from '../src/commands/status.js'
10
- import { runToken } from '../src/commands/token.js'
11
- import { runUpload } from '../src/commands/upload.js'
3
+
4
+ // Each command is imported where it runs, not here. Importing all nine up front pulled
5
+ // teleproto into every invocation — about 0.3s and 45MB — including `--help`, `config` and
6
+ // `logout`, which never open a socket. `src/cli.js` stays a static import because parsing the
7
+ // arguments is the one thing every run does, and nothing it touches reaches the network.
12
8
 
13
9
  const SIGINT_EXIT_CODE = 130
14
10
 
@@ -45,66 +41,97 @@ async function main() {
45
41
  process.stdout.write(HELP)
46
42
  return
47
43
 
48
- case 'login':
44
+ case 'login': {
45
+ const { runLogin } = await import('../src/commands/login.js')
46
+
49
47
  await runLogin({
50
48
  args: parsed.args,
51
49
  token: Boolean(parsed.options.token),
52
50
  verbose: Boolean(parsed.options.verbose),
53
51
  })
54
52
  return
53
+ }
54
+
55
+ case 'logout': {
56
+ const { runLogout } = await import('../src/commands/logout.js')
55
57
 
56
- case 'logout':
57
58
  await runLogout()
58
59
  return
60
+ }
61
+
62
+ case 'list': {
63
+ const { runList } = await import('../src/commands/list.js')
59
64
 
60
- case 'list':
61
65
  await runList(parsed.options)
62
66
  return
67
+ }
68
+
69
+ case 'status': {
70
+ const { runStatus } = await import('../src/commands/status.js')
63
71
 
64
- case 'status':
65
72
  await runStatus(parsed.options)
66
73
  return
74
+ }
75
+
76
+ case 'config': {
77
+ const { runConfig } = await import('../src/commands/config.js')
67
78
 
68
- case 'config':
69
79
  await runConfig(parsed.args, parsed.options)
70
80
  return
81
+ }
82
+
83
+ case 'token': {
84
+ const { runToken } = await import('../src/commands/token.js')
71
85
 
72
- case 'token':
73
86
  await runToken(parsed.args, parsed.options)
74
87
  return
88
+ }
89
+
90
+ case 'upload': {
91
+ const { runUpload } = await import('../src/commands/upload.js')
75
92
 
76
- case 'upload':
77
93
  await runUpload(parsed.args[0], parsed.options, {
78
94
  onBackupId: (id) => {
79
95
  currentBackupId = id
80
96
  },
81
97
  })
82
98
  return
99
+ }
83
100
 
84
- case 'restore':
101
+ case 'restore': {
85
102
  if (!parsed.args[0]) {
86
103
  throw new Error('Missing backup id. Example: npx telstore restore telstore-20260905-7f3a91')
87
104
  }
105
+
106
+ const { runRestore } = await import('../src/commands/restore.js')
107
+
88
108
  await runRestore(parsed.args[0], parsed.options)
89
109
  return
110
+ }
90
111
 
91
- case 'delete':
112
+ case 'delete': {
92
113
  if (!parsed.args[0]) {
93
114
  throw new Error('Missing backup id. Example: npx telstore delete telstore-20260905-7f3a91')
94
115
  }
116
+
117
+ const { runDelete } = await import('../src/commands/delete.js')
118
+
95
119
  await runDelete(parsed.args[0], parsed.options)
96
120
  return
121
+ }
97
122
 
98
123
  default:
99
124
  throw new Error(`Unknown command: ${parsed.command}`)
100
125
  }
101
126
  }
102
127
 
103
- // GramJS keeps "exported senders" around together with a 30-second timer to release
104
- // them, and neither client.disconnect() nor destroy() cleans them up: both of those
105
- // maps are Maps, but the code walks them with Object.values and so misses everything.
106
- // The result is a command that prints "Done" and then hangs for another ~30 seconds,
107
- // during which Ctrl-C falsely reports that nothing was saved. Finish the work, exit.
128
+ // Written for GramJS, which kept "exported senders" alive behind a 30-second release timer
129
+ // that neither disconnect() nor destroy() could clear — both walked a Map with Object.values
130
+ // and missed every entry, so a command printed "Done" and then hung for half a minute, during
131
+ // which Ctrl-C falsely reported that nothing had been saved. teleproto replaced that pool
132
+ // wholesale and closes it on destroy(). This stays anyway: a CLI that has written its last
133
+ // line has nothing left to wait for, and one stray timer in any dependency is all it takes
134
+ // for the wait to come back.
108
135
  function exitWhenFlushed(code) {
109
136
  // The empty writes exist only to borrow their callbacks: they fire after everything
110
137
  // queued earlier has flushed, so nothing is lost when stdout/stderr is not a TTY.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telstore",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Split large files into chunks and store them on Telegram",
5
5
  "keywords": [
6
6
  "telegram",
@@ -37,6 +37,6 @@
37
37
  "test": "node --test 'test/**/*.test.js'"
38
38
  },
39
39
  "dependencies": {
40
- "telegram": "^2.26.22"
40
+ "teleproto": "^1.229.0"
41
41
  }
42
42
  }
package/src/caption.js CHANGED
@@ -24,7 +24,7 @@ export function chunkCaption({ id, number, total }) {
24
24
 
25
25
  export function manifestCaption({ id, name, size, chunks, createdAt }) {
26
26
  return [
27
- `🗄 ${oneLine(name)}`,
27
+ `📄 ${oneLine(name)}`,
28
28
  `💾 ${formatBytes(size)} · ${chunks} chunk${chunks === 1 ? '' : 's'}`,
29
29
  `🆔 ${id}`,
30
30
  `📅 ${utcMinutes(createdAt)}`,
@@ -45,7 +45,7 @@ function marker(lines, emoji) {
45
45
  export function parseManifestCaption(text) {
46
46
  const lines = String(text ?? '').split('\n')
47
47
 
48
- const name = marker(lines, '🗄')
48
+ const name = marker(lines, '📄')
49
49
  const totals = marker(lines, '💾')
50
50
  const id = marker(lines, '🆔')
51
51
  const createdAt = marker(lines, '📅')
package/src/client.js CHANGED
@@ -1,14 +1,14 @@
1
- import { Api, TelegramClient } from 'telegram'
2
- import { Logger } from 'telegram/extensions/index.js'
3
- import { LogLevel } from 'telegram/extensions/Logger.js'
4
- import { StringSession } from 'telegram/sessions/index.js'
1
+ import { Api, TelegramClient } from 'teleproto'
2
+ import { Logger } from 'teleproto/extensions/index.js'
3
+ import { LogLevel } from 'teleproto/extensions/Logger.js'
4
+ import { StringSession } from 'teleproto/sessions/index.js'
5
5
 
6
6
  import { manifestFileName } from './manifest.js'
7
7
  import { withRetry } from './retry.js'
8
- import { unlockConfig } from './session.js'
8
+ import { assertLoggedIn, unlockConfig } from './session.js'
9
9
  import { DEFAULT_STALL_MS, withStallTimeout } from './stall.js'
10
10
 
11
- // GramJS narrates its version, every connection and every disconnect at info level, and
11
+ // teleproto narrates its version, every connection and every disconnect at info level, and
12
12
  // those timestamped lines land in the middle of the progress bar. The client reads this
13
13
  // logger before it prints anything, so LogLevel.NONE silences all of it; --verbose asks
14
14
  // for the running commentary back when a connection needs diagnosing.
@@ -58,7 +58,7 @@ export async function readMessageBytes(client, message) {
58
58
  }
59
59
 
60
60
  // The one place telstore removes messages from a chat, and the mirror of searchDocuments
61
- // above. GramJS has its own deleteMessages, and it is the right thing to call — it resolves
61
+ // above. teleproto has its own deleteMessages, and it is the right thing to call — it resolves
62
62
  // the peer and picks between channels.DeleteMessages and messages.DeleteMessages, which is
63
63
  // exactly the choice a fake client would never catch us getting wrong.
64
64
  //
@@ -87,7 +87,7 @@ export async function deleteMessages(client, peer, ids, options = {}) {
87
87
 
88
88
  await withRetry(
89
89
  () =>
90
- // The options object is not optional: GramJS destructures `{ revoke }` with no
90
+ // The options object is not optional: teleproto destructures `{ revoke }` with no
91
91
  // default of its own, so a two-argument call throws a TypeError before it ever
92
92
  // reaches the network. revoke is passed explicitly anyway — a backup has to go for
93
93
  // everyone who can see the chat, and that intent belongs in our code rather than in
@@ -122,18 +122,6 @@ export async function closeQuietly(client, disconnect, onWarn) {
122
122
  }
123
123
  }
124
124
 
125
- // Two shapes count as logged in: the ordinary one login writes, and the sealed blob that
126
- // "login --token" leaves, which holds the same three fields behind a passphrase. Checked
127
- // before anything asks for that passphrase, so somebody who never logged in is told so rather
128
- // than asked to type a secret for an account that is not there.
129
- export function assertLoggedIn(config) {
130
- if (config.sealed) return
131
-
132
- if (!config.session || !config.apiId || !config.apiHash) {
133
- throw new Error('Not logged in — run "npx telstore login" first.')
134
- }
135
- }
136
-
137
125
  // Every command that needs Telegram comes through here, which makes this the one place a
138
126
  // sealed session has to be opened. Doing it anywhere else would mean eight places to keep in
139
127
  // step, and a ninth command would simply forget.
@@ -1,7 +1,6 @@
1
1
  import { chatName, describeChat } from '../chat.js'
2
2
  import {
3
3
  DELETE_BATCH_SIZE,
4
- assertLoggedIn,
5
4
  closeQuietly,
6
5
  connect as realConnect,
7
6
  deleteMessages as realDeleteMessages,
@@ -12,6 +11,7 @@ import { askConfirm } from '../confirm.js'
12
11
  import { configFile, defaultConfigDir, loadConfig } from '../config.js'
13
12
  import { manifestFileName, manifestMessageIds, parseManifestJson } from '../manifest.js'
14
13
  import { formatBytes, formatDuration } from '../progress.js'
14
+ import { assertLoggedIn } from '../session.js'
15
15
  import { requireChat, resolveSettings } from '../settings.js'
16
16
  import { clearState, findStates } from '../state.js'
17
17
 
@@ -1,12 +1,12 @@
1
1
  import { MANIFEST_TAG, parseManifestCaption } from '../caption.js'
2
2
  import { chatName, describeChat } from '../chat.js'
3
3
  import {
4
- assertLoggedIn,
5
4
  closeQuietly,
6
5
  connect as realConnect,
7
6
  searchDocuments,
8
7
  } from '../client.js'
9
8
  import { configFile, defaultConfigDir, loadConfig } from '../config.js'
9
+ import { assertLoggedIn } from '../session.js'
10
10
  import { requireChat, resolveSettings } from '../settings.js'
11
11
 
12
12
  const UNKNOWN = '—'
@@ -1,5 +1,5 @@
1
- import { TelegramClient } from 'telegram'
2
- import { StringSession } from 'telegram/sessions/index.js'
1
+ import { TelegramClient } from 'teleproto'
2
+ import { StringSession } from 'teleproto/sessions/index.js'
3
3
 
4
4
  import { loadConfig, saveConfig, defaultConfigDir } from '../config.js'
5
5
  import { normalizeChatTarget } from '../chat.js'
@@ -26,11 +26,12 @@ export function describeLoginError(err) {
26
26
  return `${description} (${message})`
27
27
  }
28
28
 
29
- // GramJS starts an update loop the moment a client connects, and that loop only stops when
30
- // destroy() marks the client destroyed. disconnect() alone leaves it pinging a socket that is
31
- // already closed: every ping fails with "Error: TIMEOUT" and asks the sender to reconnect,
32
- // printed straight over the destination question login asks after signing in. Every other
33
- // command shuts down the same way, and login is the seam tests need to reach it.
29
+ // An update loop starts the moment a client connects, and destroy() is what puts the whole
30
+ // client down — the sender pools included. Under GramJS, disconnect() alone left that loop
31
+ // pinging a closed socket, every ping failing with "Error: TIMEOUT" and asking for a
32
+ // reconnect, printed straight over the destination question login asks after signing in.
33
+ // Every command shuts down through destroy() for that reason, and login is the seam tests
34
+ // need to reach it.
34
35
  const createTelegramClient = (apiId, apiHash, options) =>
35
36
  new TelegramClient(new StringSession(''), apiId, apiHash, options)
36
37
 
@@ -1,8 +1,9 @@
1
1
  import { countChunks } from '../chunking.js'
2
2
  import { describeChat } from '../chat.js'
3
- import { assertLoggedIn, closeQuietly, connect as realConnect } from '../client.js'
3
+ import { closeQuietly, connect as realConnect } from '../client.js'
4
4
  import { configFile, defaultConfigDir, loadConfig } from '../config.js'
5
5
  import { formatBytes } from '../progress.js'
6
+ import { assertLoggedIn } from '../session.js'
6
7
  import { resolveSettings } from '../settings.js'
7
8
  import { canResume, listStates } from '../state.js'
8
9
 
@@ -1,7 +1,6 @@
1
- import { assertLoggedIn } from '../client.js'
2
1
  import { defaultConfigDir, loadConfig } from '../config.js'
3
2
  import { createPrompts } from '../prompt.js'
4
- import { unlockConfig } from '../session.js'
3
+ import { assertLoggedIn, unlockConfig } from '../session.js'
5
4
  import { knownSettings } from '../settings.js'
6
5
  import { encodeToken } from '../token.js'
7
6
 
@@ -1,8 +1,8 @@
1
1
  import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
- import { Api } from 'telegram'
5
- import { CustomFile } from 'telegram/client/uploads.js'
4
+ import { Api } from 'teleproto'
5
+ import { CustomFile } from 'teleproto/client/uploads.js'
6
6
 
7
7
  import { PART_SIZE, planChunks } from '../chunking.js'
8
8
  import { chunkCaption, manifestCaption } from '../caption.js'
package/src/downloader.js CHANGED
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'
2
2
  import { read as readCallback, write as writeCallback } from 'node:fs'
3
3
  import { promisify } from 'node:util'
4
4
 
5
- import { returnBigInt } from 'telegram/Helpers.js'
5
+ import { returnBigInt } from 'teleproto/Helpers.js'
6
6
 
7
7
  import { DEFAULT_DOWNLOAD_CONCURRENCY, PART_SIZE, SLICE_SIZE } from './chunking.js'
8
8
  import { withRetry } from './retry.js'
@@ -105,10 +105,14 @@ export async function downloadToFile(
105
105
  // Iterated by hand rather than with `for await` so each part can be given a deadline:
106
106
  // a stalled stream yields nothing and raises nothing, and only a race against a timer
107
107
  // turns that silence into an error withRetry can act on. Nothing is lost by stepping
108
- // outside `for await` — GramJS's download iterator exposes `next` alone, so breaking
108
+ // outside `for await` — teleproto's download iterator exposes `next` alone, so breaking
109
109
  // out of the loop never closed anything either.
110
- const stream = client.iterDownload({
111
- file: message.media,
110
+ //
111
+ // The media goes in its own argument, not inside the options: teleproto takes
112
+ // `iterDownload(file, params)` where GramJS took a single `{ file, ... }` object, and
113
+ // the options object arriving where the file belongs is not something a fake client
114
+ // would ever refuse — it fails only against the real one, at getFileInfo.
115
+ const stream = client.iterDownload(message.media, {
112
116
  offset: returnBigInt(start + done),
113
117
  requestSize: PART_SIZE,
114
118
  })
package/src/retry.js CHANGED
@@ -10,8 +10,14 @@ function defaultSleep(ms) {
10
10
  return new Promise((resolve) => setTimeout(resolve, ms))
11
11
  }
12
12
 
13
+ // Every "wait this long" answer Telegram gives arrives as code 420 with the seconds on the
14
+ // error itself — FLOOD_WAIT_n, SLOW_MODE_WAIT_n and the rest. Those two fields are what this
15
+ // reads, not how the message is spelled: under GramJS every flood error carried the literal
16
+ // errorMessage "FLOOD", so a check for "FLOOD_WAIT" in it matched nothing and each one was
17
+ // retried on the ordinary backoff — asking again inside a ban that was still running.
18
+ // A 420 with no seconds (a frozen account) has nothing to wait for and takes the backoff.
13
19
  function floodWaitSeconds(err) {
14
- if (typeof err?.seconds === 'number' && String(err?.errorMessage ?? '').includes('FLOOD_WAIT')) {
20
+ if (err?.code === 420 && typeof err.seconds === 'number') {
15
21
  return err.seconds
16
22
  }
17
23
  return null
package/src/session.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readSecret as realReadSecret } from './prompt.js'
2
2
  import { decodeToken } from './token.js'
3
3
 
4
- // How a stored config becomes the three things GramJS needs. There are two shapes on disk —
4
+ // How a stored config becomes the three things teleproto needs. There are two shapes on disk —
5
5
  // the ordinary login, and the sealed blob `login --token` writes — and this is the one place
6
6
  // that knows the difference, so `connect` stays about Telegram and the commands stay about
7
7
  // their own narrative.
@@ -22,3 +22,16 @@ export async function unlockConfig(config, { readSecret = realReadSecret } = {})
22
22
 
23
23
  return { apiId, apiHash, session }
24
24
  }
25
+
26
+ // Two shapes count as logged in: the ordinary one login writes, and the sealed blob that
27
+ // "login --token" leaves, which holds the same three fields behind a passphrase. It lives
28
+ // here rather than beside connect because knowing both shapes is this file's whole job, and
29
+ // because `token` is an offline command: reaching for it through src/client.js pulled the
30
+ // whole of teleproto into a run that never opens a socket.
31
+ export function assertLoggedIn(config) {
32
+ if (config.sealed) return
33
+
34
+ if (!config.session || !config.apiId || !config.apiHash) {
35
+ throw new Error('Not logged in — run "npx telstore login" first.')
36
+ }
37
+ }
package/src/stall.js CHANGED
@@ -10,16 +10,18 @@ const MAX_TIMER_MS = 2 ** 31 - 1
10
10
  // A request that never comes back is not the same as one that fails, and only one of the
11
11
  // two is something withRetry can do anything about.
12
12
  //
13
- // GramJS can leave a request queued on a sender it has quietly given up on. Its own abort
14
- // path is unreachable: MTProtoSender rejects pending states only when
15
- // `_currentRetries > _reconnectRetries`, and `reconnectRetries` has no default to compare
16
- // against, so the test is never true (network/MTProtoSender.js:376). Nor does the reconnect
17
- // itself report failure — `connect()` exhausts its attempts and returns false rather than
18
- // throwing, so `_reconnect()` finishes as if it had worked and puts the request back on a
19
- // queue with no send loop left to drain it (network/MTProtoSender.js:148,795).
13
+ // This was written for a GramJS bug that abandoned requests outright: its abort path tested
14
+ // `_currentRetries > _reconnectRetries` against a `reconnectRetries` of Infinity and so never
15
+ // fired, and `_reconnect()` read a `connect()` that merely returned false as success. A real
16
+ // network cut mid-restore froze a transfer for eleven minutes on Linux and ended it without a
17
+ // printed line on Windows. teleproto closes both halves — `connect()` throws once its attempts
18
+ // are spent, and `_reconnect()` catches that and rejects every pending request.
20
19
  //
21
- // The promise then simply sits there. Nothing throws, nothing prints, and once no handle is
22
- // left the process ends mid-transfer without a word — the one outcome this project forbids.
20
+ // The deadline stays because the library was never the only way to arrive here. A server that
21
+ // accepts a request and answers nothing, a socket that stays open with nothing coming down it:
22
+ // there is no failure for withRetry to see, only silence. The timer is what turns that silence
23
+ // into an error — and, deliberately not unref'd, it is also the handle that stops the event
24
+ // loop running dry and ending a transfer without a word.
23
25
  export async function withStallTimeout(promise, ms, describe) {
24
26
  if (!(ms > 0)) return await promise
25
27
 
package/src/token.js CHANGED
@@ -46,7 +46,7 @@ function isPlainObject(value) {
46
46
  // A token is untrusted input in the same category as a manifest or a state file. Opening one
47
47
  // proves whoever made it knew the passphrase, not that they made it correctly — and the
48
48
  // unprotected format proves nothing at all. An apiId of undefined does not throw here; it
49
- // reaches GramJS and fails much later as something that reads like a network problem.
49
+ // reaches teleproto and fails much later as something that reads like a network problem.
50
50
  export function checkTokenBundle(bundle) {
51
51
  if (!isPlainObject(bundle)) {
52
52
  throw new Error(
package/src/uploader.js CHANGED
@@ -2,8 +2,8 @@ import { createHash, randomBytes } from 'node:crypto'
2
2
  import { read as readCallback } from 'node:fs'
3
3
  import { promisify } from 'node:util'
4
4
 
5
- import { Api } from 'telegram'
6
- import { readBigIntFromBuffer } from 'telegram/Helpers.js'
5
+ import { Api } from 'teleproto'
6
+ import { readBigIntFromBuffer } from 'teleproto/Helpers.js'
7
7
 
8
8
  import { DEFAULT_UPLOAD_CONCURRENCY, PART_SIZE, MAX_PARTS } from './chunking.js'
9
9
  import { withRetry } from './retry.js'
@@ -12,8 +12,8 @@ import { DEFAULT_STALL_MS, withStallTimeout } from './stall.js'
12
12
  const read = promisify(readCallback)
13
13
 
14
14
  // Telegram splits its upload API by file size: only files above 10MB may use the
15
- // "big" family. GramJS picks the same threshold (LARGE_FILE_THRESHOLD in
16
- // node_modules/telegram/client/uploads.js), and telstore follows it.
15
+ // "big" family. teleproto picks the same threshold (LARGE_FILE_THRESHOLD in
16
+ // node_modules/teleproto/client/uploads.js), and telstore follows it.
17
17
  export const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024
18
18
 
19
19
  async function readExactly(fd, length, position) {
@@ -105,9 +105,9 @@ export async function uploadRange(client, fd, options) {
105
105
  sending.push(
106
106
  withRetry(
107
107
  () =>
108
- // Same exposure as the download path: a request left on a sender GramJS has
109
- // stopped draining never settles, so without a deadline this await would hold
110
- // the batch open forever and the upload would end without a word.
108
+ // Same exposure as the download path: a request the server accepts and never
109
+ // answers settles neither way, so without a deadline this await would hold the
110
+ // batch open forever and the upload would end without a word.
111
111
  withStallTimeout(
112
112
  client.invoke(partRequest(part, bytes)),
113
113
  stallMs,