tglfs 0.1.0
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 +216 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +187 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +627 -0
- package/dist/cli.js.map +1 -0
- package/dist/crypto.d.ts +8 -0
- package/dist/crypto.js +45 -0
- package/dist/crypto.js.map +1 -0
- package/dist/download.d.ts +26 -0
- package/dist/download.js +228 -0
- package/dist/download.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/file-ops.d.ts +71 -0
- package/dist/file-ops.js +234 -0
- package/dist/file-ops.js.map +1 -0
- package/dist/gramjs.d.ts +14 -0
- package/dist/gramjs.js +70 -0
- package/dist/gramjs.js.map +1 -0
- package/dist/interactive.d.ts +11 -0
- package/dist/interactive.js +81 -0
- package/dist/interactive.js.map +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +4 -0
- package/dist/json.js.map +1 -0
- package/dist/progress.d.ts +16 -0
- package/dist/progress.js +62 -0
- package/dist/progress.js.map +1 -0
- package/dist/protocol.d.ts +5 -0
- package/dist/protocol.js +27 -0
- package/dist/protocol.js.map +1 -0
- package/dist/search.d.ts +23 -0
- package/dist/search.js +81 -0
- package/dist/search.js.map +1 -0
- package/dist/secrets.d.ts +12 -0
- package/dist/secrets.js +29 -0
- package/dist/secrets.js.map +1 -0
- package/dist/shared/archive.d.ts +9 -0
- package/dist/shared/archive.js +95 -0
- package/dist/shared/archive.js.map +1 -0
- package/dist/shared/constants.d.ts +5 -0
- package/dist/shared/constants.js +6 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/file-cards.d.ts +31 -0
- package/dist/shared/file-cards.js +103 -0
- package/dist/shared/file-cards.js.map +1 -0
- package/dist/shared/telegram-files.d.ts +84 -0
- package/dist/shared/telegram-files.js +137 -0
- package/dist/shared/telegram-files.js.map +1 -0
- package/dist/shared/upload.d.ts +44 -0
- package/dist/shared/upload.js +181 -0
- package/dist/shared/upload.js.map +1 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.js +97 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ufid.d.ts +8 -0
- package/dist/ufid.js +65 -0
- package/dist/ufid.js.map +1 -0
- package/dist/upload.d.ts +19 -0
- package/dist/upload.js +101 -0
- package/dist/upload.js.map +1 -0
- package/man/tglfs-delete.1 +19 -0
- package/man/tglfs-download.1 +42 -0
- package/man/tglfs-inspect.1 +34 -0
- package/man/tglfs-login.1 +53 -0
- package/man/tglfs-logout.1 +17 -0
- package/man/tglfs-receive.1 +26 -0
- package/man/tglfs-rename.1 +8 -0
- package/man/tglfs-search.1 +37 -0
- package/man/tglfs-send.1 +22 -0
- package/man/tglfs-status.1 +12 -0
- package/man/tglfs-unsend.1 +37 -0
- package/man/tglfs-upload.1 +28 -0
- package/man/tglfs.1 +70 -0
- package/package.json +66 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { Command, Option } from "commander";
|
|
5
|
+
import { BUNDLED_TELEGRAM_API_HASH, BUNDLED_TELEGRAM_API_ID, connectAuthorizedClient, login, logout, persistAndDisconnectClient, status, } from "./auth.js";
|
|
6
|
+
import { defaultOutputPath, downloadFileCard } from "./download.js";
|
|
7
|
+
import { CliError, EXIT_CODES, toCliError } from "./errors.js";
|
|
8
|
+
import { deleteResolvedFiles, formatDeleteConfirmation, inspectFileCard, receiveFiles, renameFile, resolveFileCardRecords, sendFiles, unsendFiles, } from "./file-ops.js";
|
|
9
|
+
import { isInteractiveSession, promptConfirm, promptOptionalText, promptSelect, promptText, } from "./interactive.js";
|
|
10
|
+
import { printJson } from "./json.js";
|
|
11
|
+
import { createByteProgressReporter } from "./progress.js";
|
|
12
|
+
import { getFileCardByUfid } from "./protocol.js";
|
|
13
|
+
import { resolveOptionalPassword } from "./secrets.js";
|
|
14
|
+
import { FILE_CARD_SEARCH_SORT_VALUES, formatSearchResultsTable, searchFileCards } from "./search.js";
|
|
15
|
+
import { formatFileCardDate, formatFileCardSize } from "./shared/file-cards.js";
|
|
16
|
+
import { storePaths } from "./store.js";
|
|
17
|
+
import { uploadPaths } from "./upload.js";
|
|
18
|
+
function emitSuccess(json, text, data) {
|
|
19
|
+
if (json) {
|
|
20
|
+
printJson({ ok: true, data });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
process.stdout.write(text + "\n");
|
|
24
|
+
}
|
|
25
|
+
function emitFailure(json, error) {
|
|
26
|
+
if (json) {
|
|
27
|
+
printJson({
|
|
28
|
+
ok: false,
|
|
29
|
+
error: {
|
|
30
|
+
code: error.code,
|
|
31
|
+
message: error.message,
|
|
32
|
+
details: error.details,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
38
|
+
}
|
|
39
|
+
async function runJsonAware(options, work) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await work();
|
|
42
|
+
emitSuccess(Boolean(options.json), result.text, result.data);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const cliError = toCliError(error);
|
|
46
|
+
emitFailure(Boolean(options.json), cliError);
|
|
47
|
+
process.exitCode = cliError.exitCode;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function pathExists(path) {
|
|
51
|
+
try {
|
|
52
|
+
await access(path);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function parsePositiveInteger(label, value) {
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
62
|
+
throw new CliError("invalid_argument", `${label} must be a positive integer.`, EXIT_CODES.GENERAL_ERROR);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
async function confirmDestructiveAction(message, force = false) {
|
|
67
|
+
if (force) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!isInteractiveSession()) {
|
|
71
|
+
throw new CliError("interactive_required", `${message} Use --yes to confirm in non-interactive mode.`, EXIT_CODES.INTERACTIVE_REQUIRED);
|
|
72
|
+
}
|
|
73
|
+
const confirmed = await promptConfirm(message);
|
|
74
|
+
if (!confirmed) {
|
|
75
|
+
throw new CliError("cancelled", "Operation cancelled.", EXIT_CODES.GENERAL_ERROR);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const TELEGRAM_PEER_HELP = [
|
|
79
|
+
"Peer resolution:",
|
|
80
|
+
" Peer values are passed directly to Telegram/GramJS entity resolution.",
|
|
81
|
+
" Users, groups, and channels can all work.",
|
|
82
|
+
" In practice, use a public username (`alice`, `@alice`, `@mygroup`, `@mychannel`), `me` for Saved Messages,",
|
|
83
|
+
" or a phone number that your Telegram account can already resolve (typically a saved contact).",
|
|
84
|
+
" Private chats/groups/channels can also work if Telegram can already resolve them for the current account,",
|
|
85
|
+
" which usually means the account already has access to that dialog/entity.",
|
|
86
|
+
" If Telegram cannot resolve the value, the command fails.",
|
|
87
|
+
].join("\n");
|
|
88
|
+
const TELEGRAM_SOURCE_HELP = [
|
|
89
|
+
TELEGRAM_PEER_HELP,
|
|
90
|
+
"",
|
|
91
|
+
"Source mailbox:",
|
|
92
|
+
" For `receive` and `unsend`, <source> is the chat/mailbox that currently contains",
|
|
93
|
+
" the TGLFS file card and chunk messages you want to operate on.",
|
|
94
|
+
" It is not necessarily the original uploader's personal account.",
|
|
95
|
+
].join("\n");
|
|
96
|
+
function formatInspectResult(result) {
|
|
97
|
+
const lines = [
|
|
98
|
+
`Peer: ${result.peer === "me" ? "Saved Messages" : result.peer}`,
|
|
99
|
+
`Name: ${result.data.name}`,
|
|
100
|
+
`UFID: ${result.data.ufid}`,
|
|
101
|
+
`Size: ${formatFileCardSize(result.data.size)}`,
|
|
102
|
+
`Date: ${formatFileCardDate(result.date)}`,
|
|
103
|
+
`Message ID: ${result.msgId}`,
|
|
104
|
+
`Status: ${result.data.uploadComplete ? "Complete" : "Incomplete"}`,
|
|
105
|
+
`Format: ${result.format}`,
|
|
106
|
+
`Chunks: ${result.chunks.length}`,
|
|
107
|
+
"",
|
|
108
|
+
"Chunk details:",
|
|
109
|
+
...result.chunks.map((chunk) => {
|
|
110
|
+
if (chunk.status === "ok") {
|
|
111
|
+
return ` ${chunk.msgId}: ok (${formatFileCardSize(chunk.size ?? 0)})`;
|
|
112
|
+
}
|
|
113
|
+
return ` ${chunk.msgId}: ${chunk.status}${chunk.className ? ` (${chunk.className})` : ""}`;
|
|
114
|
+
}),
|
|
115
|
+
];
|
|
116
|
+
if (result.probe) {
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push(`Probe: ${result.probe.mode} (${formatFileCardSize(result.probe.bytesWritten)} -> ${result.probe.computedUfid})`);
|
|
119
|
+
}
|
|
120
|
+
else if (result.probeError) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(`Probe: ${result.probeError}`);
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
async function runInteractiveMenu(program) {
|
|
127
|
+
const choice = await promptSelect("TGLFS action", [
|
|
128
|
+
{ title: "Upload", value: "upload", description: "Upload one file or an archive of multiple files." },
|
|
129
|
+
{ title: "Login", value: "login", description: "Authenticate and persist the Telegram session." },
|
|
130
|
+
{ title: "Status", value: "status", description: "Show current config and auth status." },
|
|
131
|
+
{ title: "Search", value: "search", description: "Search TGLFS file cards in Saved Messages." },
|
|
132
|
+
{ title: "Download", value: "download", description: "Download a TGLFS file by UFID." },
|
|
133
|
+
{ title: "Rename", value: "rename", description: "Rename a file card in Saved Messages." },
|
|
134
|
+
{ title: "Delete", value: "delete", description: "Delete files from Saved Messages." },
|
|
135
|
+
{ title: "Send", value: "send", description: "Send owned files to another peer." },
|
|
136
|
+
{ title: "Receive", value: "receive", description: "Receive files from another peer into Saved Messages." },
|
|
137
|
+
{ title: "Unsend", value: "unsend", description: "Delete received files from another peer mailbox." },
|
|
138
|
+
{ title: "Inspect", value: "inspect", description: "Inspect a file card. Use --probe for a full format check." },
|
|
139
|
+
{ title: "Logout", value: "logout", description: "Remove the saved Telegram session." },
|
|
140
|
+
{ title: "Help", value: "help", description: "Show general CLI help." },
|
|
141
|
+
{ title: "Exit", value: "exit", description: "Quit without doing anything." },
|
|
142
|
+
]);
|
|
143
|
+
switch (choice) {
|
|
144
|
+
case "upload": {
|
|
145
|
+
const rawPaths = await promptText("File path(s) to upload (comma-separated)");
|
|
146
|
+
const paths = rawPaths
|
|
147
|
+
.split(",")
|
|
148
|
+
.map((value) => value.trim())
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
await program.parseAsync(["node", "tglfs", "upload", ...paths], { from: "user" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
case "login":
|
|
154
|
+
await program.parseAsync(["node", "tglfs", "login"], { from: "user" });
|
|
155
|
+
return;
|
|
156
|
+
case "status":
|
|
157
|
+
await program.parseAsync(["node", "tglfs", "status"], { from: "user" });
|
|
158
|
+
return;
|
|
159
|
+
case "search": {
|
|
160
|
+
const query = await promptOptionalText("Search query (leave blank to list all)");
|
|
161
|
+
await program.parseAsync(["node", "tglfs", "search", ...(query === "" ? [] : [query])], { from: "user" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
case "download": {
|
|
165
|
+
const ufid = await promptText("UFID to download");
|
|
166
|
+
await program.parseAsync(["node", "tglfs", "download", ufid], { from: "user" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
case "rename": {
|
|
170
|
+
const ufid = await promptText("UFID to rename");
|
|
171
|
+
const newName = await promptText("New file name");
|
|
172
|
+
await program.parseAsync(["node", "tglfs", "rename", ufid, newName], { from: "user" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
case "delete": {
|
|
176
|
+
const rawUfids = await promptText("UFID(s) to delete (comma-separated)");
|
|
177
|
+
const ufids = rawUfids
|
|
178
|
+
.split(",")
|
|
179
|
+
.map((value) => value.trim())
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
await program.parseAsync(["node", "tglfs", "delete", ...ufids], { from: "user" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
case "send": {
|
|
185
|
+
const rawUfids = await promptText("UFID(s) to send (comma-separated)");
|
|
186
|
+
const recipient = await promptText("Recipient peer");
|
|
187
|
+
const ufids = rawUfids
|
|
188
|
+
.split(",")
|
|
189
|
+
.map((value) => value.trim())
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
await program.parseAsync(["node", "tglfs", "send", ...ufids, "--to", recipient], { from: "user" });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
case "receive": {
|
|
195
|
+
const source = await promptText("Source peer");
|
|
196
|
+
const rawUfids = await promptText("UFID(s) to receive (comma-separated)");
|
|
197
|
+
const ufids = rawUfids
|
|
198
|
+
.split(",")
|
|
199
|
+
.map((value) => value.trim())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
await program.parseAsync(["node", "tglfs", "receive", source, ...ufids], { from: "user" });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
case "unsend": {
|
|
205
|
+
const source = await promptText("Source peer");
|
|
206
|
+
const rawUfids = await promptText("UFID(s) to unsend (comma-separated)");
|
|
207
|
+
const ufids = rawUfids
|
|
208
|
+
.split(",")
|
|
209
|
+
.map((value) => value.trim())
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
await program.parseAsync(["node", "tglfs", "unsend", source, ...ufids], { from: "user" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case "inspect": {
|
|
215
|
+
const ufid = await promptText("UFID to inspect");
|
|
216
|
+
await program.parseAsync(["node", "tglfs", "inspect", ufid], { from: "user" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
case "logout":
|
|
220
|
+
await program.parseAsync(["node", "tglfs", "logout"], { from: "user" });
|
|
221
|
+
return;
|
|
222
|
+
case "help":
|
|
223
|
+
program.outputHelp();
|
|
224
|
+
return;
|
|
225
|
+
case "exit":
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function main(argv) {
|
|
230
|
+
const program = new Command();
|
|
231
|
+
program
|
|
232
|
+
.name("tglfs")
|
|
233
|
+
.description("Authenticate with Telegram, manage TGLFS file cards, transfer files between peers, and download current or legacy TGLFS files by UFID.")
|
|
234
|
+
.showHelpAfterError()
|
|
235
|
+
.showSuggestionAfterError()
|
|
236
|
+
.addHelpCommand("help [command]", "display help for command")
|
|
237
|
+
.addHelpText("after", `\nStorage paths:\n config: ${storePaths.configFile}\n session: ${storePaths.sessionFile}\n`);
|
|
238
|
+
program
|
|
239
|
+
.command("upload")
|
|
240
|
+
.description("Upload one file, or archive multiple files, into Telegram Saved Messages.")
|
|
241
|
+
.argument("<paths...>", "File path(s) to upload")
|
|
242
|
+
.option("--password <password>", "Encryption password")
|
|
243
|
+
.option("--password-env [name]", "Read encryption password from an environment variable")
|
|
244
|
+
.option("--password-stdin", "Read encryption password from stdin")
|
|
245
|
+
.option("--json", "Output machine-readable JSON")
|
|
246
|
+
.addHelpText("after", "\nUploading multiple paths produces a tar archive using the same naming convention as the web app.\nTTY runs show separate UFID and upload progress bars; --json stays quiet for automation.\nEnvironment variable: TGLFS_UPLOAD_PASSWORD\n")
|
|
247
|
+
.action(async (paths, options) => {
|
|
248
|
+
await runJsonAware(options, async () => {
|
|
249
|
+
const { client, config, session } = await connectAuthorizedClient();
|
|
250
|
+
let ufidProgress;
|
|
251
|
+
let uploadProgress;
|
|
252
|
+
try {
|
|
253
|
+
const password = (await resolveOptionalPassword({
|
|
254
|
+
...options,
|
|
255
|
+
defaultEnv: "TGLFS_UPLOAD_PASSWORD",
|
|
256
|
+
promptMessage: "File encryption password (leave empty if none)",
|
|
257
|
+
stdinMessage: "File encryption password is required on stdin.",
|
|
258
|
+
fallbackValue: "",
|
|
259
|
+
})) ?? "";
|
|
260
|
+
const result = await uploadPaths(client, {
|
|
261
|
+
paths,
|
|
262
|
+
chunkSize: config.chunkSize,
|
|
263
|
+
password,
|
|
264
|
+
onUfidProgress: ({ bytesProcessed, totalBytes }) => {
|
|
265
|
+
if (options.json) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
ufidProgress ??= createByteProgressReporter({
|
|
269
|
+
label: "Calculating UFID",
|
|
270
|
+
totalBytes,
|
|
271
|
+
});
|
|
272
|
+
ufidProgress.update(bytesProcessed);
|
|
273
|
+
},
|
|
274
|
+
onUploadProgress: ({ bytesProcessed, totalBytes }) => {
|
|
275
|
+
if (options.json) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (ufidProgress) {
|
|
279
|
+
ufidProgress.complete();
|
|
280
|
+
ufidProgress = undefined;
|
|
281
|
+
}
|
|
282
|
+
uploadProgress ??= createByteProgressReporter({
|
|
283
|
+
label: "Uploading",
|
|
284
|
+
totalBytes,
|
|
285
|
+
});
|
|
286
|
+
uploadProgress.update(bytesProcessed);
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
ufidProgress?.complete();
|
|
290
|
+
uploadProgress?.complete();
|
|
291
|
+
await persistAndDisconnectClient(client, session);
|
|
292
|
+
return {
|
|
293
|
+
text: result.archived
|
|
294
|
+
? `Uploaded archive ${result.name} as UFID ${result.ufid}.`
|
|
295
|
+
: `Uploaded ${result.name} as UFID ${result.ufid}.`,
|
|
296
|
+
data: result,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
ufidProgress?.abort();
|
|
301
|
+
uploadProgress?.abort();
|
|
302
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
program
|
|
308
|
+
.command("login")
|
|
309
|
+
.description("Authenticate with Telegram and persist the session for future commands.")
|
|
310
|
+
.option("--api-id <id>", "Telegram API ID")
|
|
311
|
+
.option("--api-hash <hash>", "Telegram API hash")
|
|
312
|
+
.option("--phone <phone>", "Telegram phone number")
|
|
313
|
+
.option("--code <code>", "Telegram login code")
|
|
314
|
+
.option("--code-stdin", "Read the Telegram login code from stdin")
|
|
315
|
+
.option("--password <password>", "Telegram 2FA password")
|
|
316
|
+
.option("--password-stdin", "Read the Telegram 2FA password from stdin")
|
|
317
|
+
.option("--json", "Output machine-readable JSON")
|
|
318
|
+
.addHelpText("after", `\nDefaults:\n API ID: ${BUNDLED_TELEGRAM_API_ID}\n API hash: ${BUNDLED_TELEGRAM_API_HASH}\n\nEnvironment variables:\n TGLFS_API_ID\n TGLFS_API_HASH\n TGLFS_PHONE\n TGLFS_LOGIN_CODE\n TGLFS_2FA_PASSWORD\n`)
|
|
319
|
+
.action(async (options) => {
|
|
320
|
+
await runJsonAware(options, async () => {
|
|
321
|
+
const result = await login(options);
|
|
322
|
+
return {
|
|
323
|
+
text: `Logged in as ${result.me.firstName ?? result.me.username ?? result.me.id}. Session saved to ${result.paths.sessionFile}.`,
|
|
324
|
+
data: result,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
program
|
|
329
|
+
.command("status")
|
|
330
|
+
.description("Show persisted config and Telegram authorization status.")
|
|
331
|
+
.option("--json", "Output machine-readable JSON")
|
|
332
|
+
.action(async (options) => {
|
|
333
|
+
await runJsonAware(options, async () => {
|
|
334
|
+
const result = await status();
|
|
335
|
+
const summary = result.authorized
|
|
336
|
+
? `Authorized as ${result.identity?.firstName ?? result.identity?.username ?? result.identity?.id}.`
|
|
337
|
+
: result.configured
|
|
338
|
+
? "Configured, but not currently authorized."
|
|
339
|
+
: "Not configured.";
|
|
340
|
+
return {
|
|
341
|
+
text: summary,
|
|
342
|
+
data: result,
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
program
|
|
347
|
+
.command("logout")
|
|
348
|
+
.description("Remove the saved Telegram session. Use --all to also remove config.")
|
|
349
|
+
.option("--all", "Also delete persisted config")
|
|
350
|
+
.option("--json", "Output machine-readable JSON")
|
|
351
|
+
.action(async (options) => {
|
|
352
|
+
await runJsonAware(options, async () => {
|
|
353
|
+
const result = await logout(Boolean(options.all));
|
|
354
|
+
return {
|
|
355
|
+
text: options.all ? "Removed saved session and config." : "Removed saved session.",
|
|
356
|
+
data: result,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
program
|
|
361
|
+
.command("search")
|
|
362
|
+
.description("Search TGLFS file cards in Telegram Saved Messages or another peer mailbox.")
|
|
363
|
+
.argument("[query]", "Search query for filename or UFID")
|
|
364
|
+
.addOption(new Option("--sort <sort>", "Sort order for the current result window")
|
|
365
|
+
.choices([...FILE_CARD_SEARCH_SORT_VALUES])
|
|
366
|
+
.default(FILE_CARD_SEARCH_SORT_VALUES[0]))
|
|
367
|
+
.option("--peer <peer>", "Peer to search instead of Saved Messages")
|
|
368
|
+
.option("--limit <n>", "Maximum number of file cards to fetch", (value) => parsePositiveInteger("Limit", value), 50)
|
|
369
|
+
.option("--offset-id <msgId>", "Resume from a Telegram message-id cursor", (value) => parsePositiveInteger("Offset id", value))
|
|
370
|
+
.option("--json", "Output machine-readable JSON")
|
|
371
|
+
.addHelpText("after", "\nIf no query is provided, the command lists the first page of all TGLFS file cards in Saved Messages.\nPagination uses Telegram message ids via --offset-id.\n")
|
|
372
|
+
.action(async (query, options) => {
|
|
373
|
+
await runJsonAware(options, async () => {
|
|
374
|
+
const { client, session } = await connectAuthorizedClient();
|
|
375
|
+
try {
|
|
376
|
+
const result = await searchFileCards(client, {
|
|
377
|
+
peer: options.peer,
|
|
378
|
+
query,
|
|
379
|
+
limit: options.limit,
|
|
380
|
+
offsetId: options.offsetId,
|
|
381
|
+
sort: options.sort,
|
|
382
|
+
});
|
|
383
|
+
await persistAndDisconnectClient(client, session);
|
|
384
|
+
return {
|
|
385
|
+
text: formatSearchResultsTable(result),
|
|
386
|
+
data: result,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
program
|
|
396
|
+
.command("download")
|
|
397
|
+
.description("Download a TGLFS file from Telegram by UFID.")
|
|
398
|
+
.argument("<ufid>", "TGLFS file UFID")
|
|
399
|
+
.option("-o, --output <path>", "Destination file path")
|
|
400
|
+
.option("-f, --force", "Overwrite the output path if it already exists")
|
|
401
|
+
.option("--legacy", "Use the legacy decryption/counter pipeline")
|
|
402
|
+
.option("--password <password>", "Decryption password")
|
|
403
|
+
.option("--password-env [name]", "Read decryption password from an environment variable")
|
|
404
|
+
.option("--password-stdin", "Read decryption password from stdin")
|
|
405
|
+
.option("--json", "Output machine-readable JSON")
|
|
406
|
+
.addHelpText("after", "\nIf the file uses a decryption password, provide it with --password, --password-env, --password-stdin, or interactively on a TTY.\nTTY runs show a progress bar; --json stays quiet for automation.\n")
|
|
407
|
+
.action(async (ufid, options) => {
|
|
408
|
+
await runJsonAware(options, async () => {
|
|
409
|
+
const { client, session } = await connectAuthorizedClient();
|
|
410
|
+
let progress;
|
|
411
|
+
try {
|
|
412
|
+
const record = await getFileCardByUfid(client, ufid);
|
|
413
|
+
const outputPath = options.output ? String(options.output) : defaultOutputPath(record.data.name);
|
|
414
|
+
if ((await pathExists(outputPath)) && !options.force) {
|
|
415
|
+
if (!isInteractiveSession()) {
|
|
416
|
+
throw new CliError("output_exists", `Output path already exists: ${outputPath}. Use --force to overwrite it.`, EXIT_CODES.GENERAL_ERROR);
|
|
417
|
+
}
|
|
418
|
+
const overwrite = await promptConfirm(`Overwrite existing file at ${outputPath}?`);
|
|
419
|
+
if (!overwrite) {
|
|
420
|
+
throw new CliError("cancelled", "Download cancelled.", EXIT_CODES.GENERAL_ERROR);
|
|
421
|
+
}
|
|
422
|
+
options.force = true;
|
|
423
|
+
}
|
|
424
|
+
const password = (await resolveOptionalPassword({
|
|
425
|
+
...options,
|
|
426
|
+
defaultEnv: "TGLFS_DOWNLOAD_PASSWORD",
|
|
427
|
+
promptMessage: "File decryption password (leave empty if none)",
|
|
428
|
+
stdinMessage: "File decryption password is required on stdin.",
|
|
429
|
+
fallbackValue: "",
|
|
430
|
+
})) ?? "";
|
|
431
|
+
progress = options.json
|
|
432
|
+
? undefined
|
|
433
|
+
: createByteProgressReporter({
|
|
434
|
+
label: "Downloading",
|
|
435
|
+
totalBytes: record.data.size,
|
|
436
|
+
});
|
|
437
|
+
progress?.update(0);
|
|
438
|
+
const result = await downloadFileCard(client, record.data, password, outputPath, Boolean(options.force), ({ bytesWritten }) => progress?.update(bytesWritten), options.legacy ? "legacy" : "current");
|
|
439
|
+
progress?.complete();
|
|
440
|
+
await persistAndDisconnectClient(client, session);
|
|
441
|
+
return {
|
|
442
|
+
text: `Downloaded ${result.name} to ${result.outputPath}${options.legacy ? " using the legacy pipeline" : ""}.`,
|
|
443
|
+
data: result,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
progress?.abort();
|
|
448
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
program
|
|
454
|
+
.command("rename")
|
|
455
|
+
.description("Rename a TGLFS file card in Saved Messages.")
|
|
456
|
+
.argument("<ufid>", "TGLFS file UFID")
|
|
457
|
+
.argument("<new-name>", "New file name")
|
|
458
|
+
.option("--json", "Output machine-readable JSON")
|
|
459
|
+
.action(async (ufid, newName, options) => {
|
|
460
|
+
await runJsonAware(options, async () => {
|
|
461
|
+
const { client, session } = await connectAuthorizedClient();
|
|
462
|
+
try {
|
|
463
|
+
const result = await renameFile(client, ufid, newName);
|
|
464
|
+
await persistAndDisconnectClient(client, session);
|
|
465
|
+
return {
|
|
466
|
+
text: `Renamed ${result.before.data.ufid} to ${result.after.data.name}.`,
|
|
467
|
+
data: result,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
program
|
|
477
|
+
.command("delete")
|
|
478
|
+
.description("Delete one or more owned TGLFS files from Saved Messages.")
|
|
479
|
+
.argument("<ufids...>", "UFID(s) to delete")
|
|
480
|
+
.option("-y, --yes", "Skip the confirmation prompt")
|
|
481
|
+
.option("--json", "Output machine-readable JSON")
|
|
482
|
+
.action(async (ufids, options) => {
|
|
483
|
+
await runJsonAware(options, async () => {
|
|
484
|
+
const { client, session } = await connectAuthorizedClient();
|
|
485
|
+
try {
|
|
486
|
+
const records = await resolveFileCardRecords(client, ufids, "me");
|
|
487
|
+
await confirmDestructiveAction(formatDeleteConfirmation(records), Boolean(options.yes));
|
|
488
|
+
const result = await deleteResolvedFiles(client, records);
|
|
489
|
+
await persistAndDisconnectClient(client, session);
|
|
490
|
+
return {
|
|
491
|
+
text: `Deleted ${result.length} file(s) from Saved Messages.`,
|
|
492
|
+
data: { count: result.length, files: result },
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
program
|
|
502
|
+
.command("send")
|
|
503
|
+
.description("Send one or more owned TGLFS files to another peer.")
|
|
504
|
+
.argument("<ufids...>", "UFID(s) to send")
|
|
505
|
+
.requiredOption("--to <peer>", "Recipient peer/mailbox")
|
|
506
|
+
.addHelpText("after", `\n${TELEGRAM_PEER_HELP}\n`)
|
|
507
|
+
.option("--json", "Output machine-readable JSON")
|
|
508
|
+
.action(async (ufids, options) => {
|
|
509
|
+
await runJsonAware(options, async () => {
|
|
510
|
+
const { client, session } = await connectAuthorizedClient();
|
|
511
|
+
try {
|
|
512
|
+
const result = await sendFiles(client, ufids, options.to);
|
|
513
|
+
await persistAndDisconnectClient(client, session);
|
|
514
|
+
return {
|
|
515
|
+
text: `Sent ${result.length} file(s) to ${options.to}.`,
|
|
516
|
+
data: { recipient: options.to, count: result.length, files: result },
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
program
|
|
526
|
+
.command("receive")
|
|
527
|
+
.description("Receive one or more TGLFS files from another peer into Saved Messages.")
|
|
528
|
+
.argument("<source>", "Source peer/mailbox to search")
|
|
529
|
+
.argument("<ufids...>", "UFID(s) to receive")
|
|
530
|
+
.addHelpText("after", `\n${TELEGRAM_SOURCE_HELP}\n`)
|
|
531
|
+
.option("--json", "Output machine-readable JSON")
|
|
532
|
+
.action(async (source, ufids, options) => {
|
|
533
|
+
await runJsonAware(options, async () => {
|
|
534
|
+
const { client, session } = await connectAuthorizedClient();
|
|
535
|
+
try {
|
|
536
|
+
const result = await receiveFiles(client, source, ufids);
|
|
537
|
+
await persistAndDisconnectClient(client, session);
|
|
538
|
+
return {
|
|
539
|
+
text: `Received ${result.length} file(s) from ${source}.`,
|
|
540
|
+
data: { source, count: result.length, files: result },
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
program
|
|
550
|
+
.command("unsend")
|
|
551
|
+
.description("Delete one or more received TGLFS files from another peer mailbox.")
|
|
552
|
+
.argument("<source>", "Source peer/mailbox to search")
|
|
553
|
+
.argument("<ufids...>", "UFID(s) to unsend")
|
|
554
|
+
.addHelpText("after", `\n${TELEGRAM_SOURCE_HELP}\n`)
|
|
555
|
+
.option("-y, --yes", "Skip the confirmation prompt")
|
|
556
|
+
.option("--json", "Output machine-readable JSON")
|
|
557
|
+
.action(async (source, ufids, options) => {
|
|
558
|
+
await runJsonAware(options, async () => {
|
|
559
|
+
await confirmDestructiveAction(`Unsend ${ufids.length} file(s) from ${source}?`, Boolean(options.yes));
|
|
560
|
+
const { client, session } = await connectAuthorizedClient();
|
|
561
|
+
try {
|
|
562
|
+
const result = await unsendFiles(client, source, ufids);
|
|
563
|
+
await persistAndDisconnectClient(client, session);
|
|
564
|
+
return {
|
|
565
|
+
text: `Unsent ${result.length} file(s) from ${source}.`,
|
|
566
|
+
data: { source, count: result.length, files: result },
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
program
|
|
576
|
+
.command("inspect")
|
|
577
|
+
.description("Inspect a file card and chunk references. Use --probe for a full current-vs-legacy integrity probe.")
|
|
578
|
+
.argument("<ufid>", "TGLFS file UFID")
|
|
579
|
+
.option("--peer <peer>", "Peer to inspect instead of Saved Messages")
|
|
580
|
+
.option("--probe", "Run the full current-vs-legacy probe. This downloads, decrypts, and validates file data.")
|
|
581
|
+
.option("--password <password>", "Password to use for current-vs-legacy probing")
|
|
582
|
+
.option("--password-env [name]", "Read the probe password from an environment variable")
|
|
583
|
+
.option("--password-stdin", "Read the probe password from stdin")
|
|
584
|
+
.option("--json", "Output machine-readable JSON")
|
|
585
|
+
.action(async (ufid, options) => {
|
|
586
|
+
await runJsonAware(options, async () => {
|
|
587
|
+
const { client, session } = await connectAuthorizedClient();
|
|
588
|
+
try {
|
|
589
|
+
const shouldProbe = Boolean(options.probe || options.password !== undefined || options.passwordEnv || options.passwordStdin);
|
|
590
|
+
const password = shouldProbe
|
|
591
|
+
? await resolveOptionalPassword({
|
|
592
|
+
...options,
|
|
593
|
+
defaultEnv: "TGLFS_INSPECT_PASSWORD",
|
|
594
|
+
promptMessage: "File probe password (leave empty if none)",
|
|
595
|
+
stdinMessage: "File probe password is required on stdin.",
|
|
596
|
+
fallbackValue: "",
|
|
597
|
+
})
|
|
598
|
+
: undefined;
|
|
599
|
+
const result = await inspectFileCard(client, ufid, {
|
|
600
|
+
peer: options.peer,
|
|
601
|
+
password,
|
|
602
|
+
probe: shouldProbe,
|
|
603
|
+
});
|
|
604
|
+
await persistAndDisconnectClient(client, session);
|
|
605
|
+
return {
|
|
606
|
+
text: formatInspectResult(result),
|
|
607
|
+
data: result,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
await persistAndDisconnectClient(client, session).catch(() => { });
|
|
612
|
+
throw error;
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
if (argv.length <= 2 && isInteractiveSession()) {
|
|
617
|
+
await runInteractiveMenu(program);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
await program.parseAsync(argv);
|
|
621
|
+
}
|
|
622
|
+
main(process.argv).catch((error) => {
|
|
623
|
+
const cliError = toCliError(error);
|
|
624
|
+
emitFailure(false, cliError);
|
|
625
|
+
process.exit(cliError.exitCode);
|
|
626
|
+
});
|
|
627
|
+
//# sourceMappingURL=cli.js.map
|