supertelegram 0.4.0 → 0.6.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.
@@ -0,0 +1,52 @@
1
+ name: publish
2
+
3
+ # publishes to npm when you push a version tag (e.g. v0.5.0) or cut a GitHub release.
4
+ # tag the current version with: git tag v$(node -p "require('./package.json').version") && git push --tags
5
+ # auth is via npm OIDC trusted publishing (no NPM_TOKEN) — configure a trusted
6
+ # publisher for this package on npmjs.com pointing at this repo + workflow.
7
+
8
+ on:
9
+ push:
10
+ tags:
11
+ - "v*"
12
+ release:
13
+ types: [published]
14
+ workflow_dispatch:
15
+
16
+ jobs:
17
+ publish:
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ contents: read
21
+ id-token: write # enables npm provenance
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: oven-sh/setup-bun@v2
26
+ with:
27
+ bun-version: latest
28
+
29
+ - name: install deps
30
+ run: bun install --frozen-lockfile
31
+
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version: 24
35
+ registry-url: "https://registry.npmjs.org"
36
+
37
+ # OIDC trusted publishing needs a recent npm (node 24 ships one, but pin latest)
38
+ - name: upgrade npm
39
+ run: npm install -g npm@latest
40
+
41
+ - name: guard — tag matches package.json version
42
+ if: startsWith(github.ref, 'refs/tags/')
43
+ run: |
44
+ PKG_VERSION="v$(node -p "require('./package.json').version")"
45
+ if [ "$PKG_VERSION" != "${GITHUB_REF_NAME}" ]; then
46
+ echo "::error::tag ${GITHUB_REF_NAME} does not match package.json ($PKG_VERSION)"
47
+ exit 1
48
+ fi
49
+
50
+ # no NODE_AUTH_TOKEN: npm authenticates via OIDC (id-token: write above)
51
+ - name: publish
52
+ run: npm publish --access public --provenance
package/PUBLISHING.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # publishing guide
2
2
 
3
- ## setup
3
+ ## automated (github actions) — preferred
4
+
5
+ CI publishes to npm on every version tag. auth is via **OIDC trusted
6
+ publishing** — no `NPM_TOKEN` secret. one-time setup: on npmjs.com, open the
7
+ package → Settings → Trusted Publishing, and add a GitHub Actions publisher
8
+ pointing at `caffeinum/supertelegram`, workflow `publish.yml`.
9
+
10
+ then to release:
11
+ ```bash
12
+ # bump version in package.json first, commit, then:
13
+ git tag "v$(node -p "require('./package.json').version")"
14
+ git push --tags
15
+ ```
16
+ the `.github/workflows/publish.yml` workflow checks the tag matches
17
+ package.json, installs, and runs `npm publish --access public --provenance`.
18
+ you can also trigger it manually from the Actions tab (workflow_dispatch).
19
+
20
+ ## manual setup
4
21
 
5
22
  1. login to npm:
6
23
  ```bash
package/README.md CHANGED
@@ -78,15 +78,42 @@ telegram config set appHash "abc123..."
78
78
  - `--help` - show help
79
79
  - `--version` - show version
80
80
 
81
+ ### multiple accounts
82
+
83
+ log in to as many accounts as you want, each stored under a name, and switch
84
+ between them:
85
+
86
+ ```bash
87
+ # log into named accounts (prompts phone/code the first time)
88
+ telegram login personal
89
+ telegram login work
90
+
91
+ # see them (* marks the active one)
92
+ telegram accounts
93
+ # * work — @yourworkhandle
94
+ # personal — @yourhandle
95
+
96
+ # switch the active account (all later commands use it)
97
+ telegram switch personal
98
+ telegram whoami # personal — @yourhandle
99
+
100
+ # or run a single command as another account without switching
101
+ telegram -a work send @boss "on it"
102
+
103
+ # remove an account
104
+ telegram logout work
105
+ ```
106
+
107
+ sessions live in `~/.supertelegram/accounts/<name>.txt`; the active account is
108
+ tracked in `~/.supertelegram/accounts.json`. API credentials (appId/appHash)
109
+ are shared across accounts. upgrading from an older version? your existing
110
+ login is migrated automatically into an account named `default`.
111
+
81
112
  ### advanced
82
113
 
83
- **multi-account / custom session location:**
114
+ **custom session location (one-off / scripting):**
84
115
  ```bash
85
- # use env var
86
116
  TELEGRAM_SESSION=./custom.txt telegram send @friend "hey"
87
-
88
- # or pass flag (todo)
89
- telegram send @friend "hey" --session ./custom.txt
90
117
  ```
91
118
 
92
119
  **precedence for API credentials:**
@@ -95,9 +122,27 @@ telegram send @friend "hey" --session ./custom.txt
95
122
  3. `.env` file in current directory (for dev)
96
123
 
97
124
  **precedence for session file:**
98
- 1. `TELEGRAM_SESSION` env var
99
- 2. `~/.supertelegram/session.txt` (global default)
100
- 3. `./session.txt` (backwards compat)
125
+ 1. `--account <name>` flag
126
+ 2. `TELEGRAM_SESSION` env var
127
+ 3. active account (`~/.supertelegram/accounts/<name>.txt`)
128
+ 4. `./session.txt` (backwards compat)
129
+ 5. `~/.supertelegram/session.txt` (legacy default)
130
+
131
+ ## websocket transport (blocked mtproto)
132
+
133
+ some networks (cloud sandboxes, agent runtimes, corporate proxies) let tcp reach
134
+ telegram DC ips but kill the raw mtproto handshake — login dies with
135
+ `Not connected` on `ReqPqMulti`. switch to the same wss path web telegram uses:
136
+
137
+ ```bash
138
+ TELEGRAM_WSS=1 telegram login
139
+ # or persist it
140
+ telegram config set wss true
141
+ ```
142
+
143
+ this talks to `*.web.telegram.org/apiws` over tls 443 with the obfuscated
144
+ transport. login, messages and media all work; only the transport changes.
145
+ needs bun or node >= 22 (native WebSocket).
101
146
 
102
147
  ## development
103
148
 
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "supertelegram",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "telegram cli for humans and bots",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/caffeinum/supertelegram.git"
10
+ },
11
+ "homepage": "https://github.com/caffeinum/supertelegram#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/caffeinum/supertelegram/issues"
14
+ },
7
15
  "bin": {
8
16
  "telegram": "src/cli/run.ts"
9
17
  },
@@ -10,7 +10,16 @@ import {
10
10
  downloadMedia as telegramDownloadMedia,
11
11
  } from "../client/telegram";
12
12
  import { askPhoneNumber, askPhoneCode, askPassword, askAppId, askAppHash } from "./prompts";
13
- import { getApiCredentials, setConfig } from "../config/manager";
13
+ import { getApiCredentials, getConfig, setConfig } from "../config/manager";
14
+ import {
15
+ listAccounts,
16
+ setCurrentAccount,
17
+ registerAccount,
18
+ removeAccount,
19
+ getCurrentAccount,
20
+ accountSessionPath,
21
+ } from "../config/accounts";
22
+ import { setSessionPath } from "../client/telegram";
14
23
  import { Api } from "telegram";
15
24
 
16
25
  export async function send(username: string, message: string) {
@@ -160,37 +169,118 @@ export async function reply(chatName: string, message: string) {
160
169
  await disconnect();
161
170
  }
162
171
 
163
- export async function login() {
164
- // check if API credentials are configured
172
+ export async function login(accountName?: string) {
173
+ const name = accountName || getCurrentAccount() || "default";
174
+ // isolate this login to the named account's own session file
175
+ setSessionPath(accountSessionPath(name));
176
+
177
+ // check if API credentials are configured (shared across accounts)
165
178
  const creds = getApiCredentials();
166
179
  if (!creds) {
167
180
  console.log("no API credentials found. let's set them up first.");
168
181
  const appId = await askAppId();
169
182
  const appHash = await askAppHash();
170
-
183
+
171
184
  setConfig("appId", appId);
172
185
  setConfig("appHash", appHash);
173
186
  console.log("credentials saved to ~/.supertelegram/config.json\n");
174
187
  }
175
188
 
176
189
  const loggedIn = await isLoggedIn();
177
- if (loggedIn) {
178
- console.log("already logged in!");
179
- await disconnect();
180
- return;
190
+ if (!loggedIn) {
191
+ console.log(`starting login for account "${name}"...`);
192
+ await telegramLogin({
193
+ phoneNumber: askPhoneNumber,
194
+ phoneCode: askPhoneCode,
195
+ password: askPassword,
196
+ });
197
+ } else {
198
+ console.log(`account "${name}" already has a session, refreshing details...`);
181
199
  }
182
200
 
183
- console.log("starting login...");
184
- await telegramLogin({
185
- phoneNumber: askPhoneNumber,
186
- phoneCode: askPhoneCode,
187
- password: askPassword,
201
+ // capture identity + register (and make current)
202
+ const client = await getClient();
203
+ const me = await client.getMe();
204
+ const fullName = [me.firstName, me.lastName].filter(Boolean).join(" ");
205
+ registerAccount(name, {
206
+ username: me.username ?? undefined,
207
+ userId: me.id?.toString(),
208
+ name: fullName || undefined,
188
209
  });
189
210
 
190
- console.log("login complete!");
211
+ const label = me.username ? `@${me.username}` : fullName || name;
212
+ console.log(`logged in as ${label} — account "${name}" is now active`);
191
213
  await disconnect();
192
214
  }
193
215
 
216
+ export async function accounts() {
217
+ const list = listAccounts();
218
+ if (list.length === 0) {
219
+ console.log("no accounts yet. run: telegram login <name>");
220
+ return;
221
+ }
222
+ for (const { name, meta, current } of list) {
223
+ const marker = current ? "*" : " ";
224
+ const who = meta.username
225
+ ? `@${meta.username}`
226
+ : meta.name || (meta.userId ? `id ${meta.userId}` : "");
227
+ console.log(`${marker} ${name}${who ? ` — ${who}` : ""}`);
228
+ }
229
+ console.log("\nswitch with: telegram switch <name>");
230
+ }
231
+
232
+ export async function switchAccount(name?: string) {
233
+ if (!name) {
234
+ console.error("usage: telegram switch <name>");
235
+ console.error("see accounts with: telegram accounts");
236
+ process.exit(1);
237
+ }
238
+ setCurrentAccount(name);
239
+ console.log(`switched to account "${name}"`);
240
+ }
241
+
242
+ export async function logout(name?: string) {
243
+ const target = name || getCurrentAccount();
244
+ if (!target) {
245
+ console.error("no account to log out. see: telegram accounts");
246
+ process.exit(1);
247
+ }
248
+ removeAccount(target);
249
+ const now = getCurrentAccount();
250
+ console.log(
251
+ `logged out of "${target}"` + (now ? `. active account is now "${now}"` : ". no accounts left")
252
+ );
253
+ }
254
+
255
+ export async function whoami() {
256
+ const current = getCurrentAccount();
257
+ if (!current) {
258
+ console.log("not logged in. run: telegram login");
259
+ return;
260
+ }
261
+
262
+ let meta = listAccounts().find((a) => a.name === current)?.meta;
263
+
264
+ // backfill identity for accounts migrated without metadata
265
+ if (meta && !meta.username && !meta.name && (await isLoggedIn())) {
266
+ const client = await getClient();
267
+ const me = await client.getMe();
268
+ const fullName = [me.firstName, me.lastName].filter(Boolean).join(" ");
269
+ meta = {
270
+ username: me.username ?? undefined,
271
+ userId: me.id?.toString(),
272
+ name: fullName || undefined,
273
+ };
274
+ registerAccount(current, meta, false);
275
+ await disconnect();
276
+ }
277
+
278
+ const who = meta?.username
279
+ ? `@${meta.username}`
280
+ : meta?.name || (meta?.userId ? `id ${meta.userId}` : "unknown");
281
+ console.log(`${current} — ${who}`);
282
+ }
283
+
194
284
  export async function config(action?: string, key?: string, value?: string) {
195
285
  if (action === "set" && key && value) {
196
286
  setConfig(key, value);
@@ -204,6 +294,8 @@ export async function config(action?: string, key?: string, value?: string) {
204
294
  console.log(cfg.appId);
205
295
  } else if (key === "appHash" && cfg) {
206
296
  console.log(cfg.appHash);
297
+ } else if (key === "wss") {
298
+ console.log(String(getConfig().wss === "true"));
207
299
  } else {
208
300
  console.log("not found");
209
301
  }
@@ -213,6 +305,7 @@ export async function config(action?: string, key?: string, value?: string) {
213
305
  console.log("usage:");
214
306
  console.log(" telegram config set appId <id>");
215
307
  console.log(" telegram config set appHash <hash>");
308
+ console.log(" telegram config set wss true (websocket transport for networks blocking mtproto)");
216
309
  console.log(" telegram config get appId");
217
310
  }
218
311
 
package/src/cli/run.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env bun
2
- import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia } from "./commands";
2
+ import { explainConnectionError } from "../client/wss";
3
+ import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia, accounts, switchAccount, logout, whoami } from "./commands";
3
4
  import { setVerbose, setSessionPath } from "../client/telegram";
5
+ import { migrateLegacyIfNeeded, accountSessionPath } from "../config/accounts";
4
6
  import pkg from "../../package.json";
5
7
 
6
8
  const VERSION = pkg.version;
@@ -20,10 +22,15 @@ commands:
20
22
  reply <chat> <message> reply to a chat by name (partial match)
21
23
  dialogs [limit] list recent dialogs (default: 10)
22
24
  unread [limit] show unread messages as json (default: 20)
23
- login authenticate with telegram
24
- config set <key> <val> set API credentials (appId, appHash)
25
+ login [name] authenticate with telegram (into a named account)
26
+ accounts list logged-in accounts (* = current)
27
+ switch <name> switch the active account
28
+ whoami show the active account
29
+ logout [name] remove an account (default: current)
30
+ config set <key> <val> set API credentials (appId, appHash) or wss true
25
31
 
26
32
  options:
33
+ -a, --account <name> run this command as a specific account
27
34
  -v, --verbose show debug logs
28
35
  -h, --help show this help
29
36
  --version show version
@@ -36,6 +43,9 @@ examples:
36
43
  ${NAME} reply "John" "hey!"
37
44
  ${NAME} unread
38
45
  ${NAME} dialogs 20
46
+ ${NAME} login work # log into a second account named "work"
47
+ ${NAME} switch work # make it active
48
+ ${NAME} -a personal unread # run one command as another account
39
49
  `.trim();
40
50
 
41
51
  const rawArgs = process.argv.slice(2);
@@ -52,6 +62,15 @@ if (rawArgs.includes("--version")) {
52
62
  }
53
63
 
54
64
  const verbose = rawArgs.includes("--verbose") || rawArgs.includes("-v");
65
+
66
+ // pull out --account/-a <name> before positional parsing
67
+ let accountFlag: string | undefined;
68
+ const accIdx = rawArgs.findIndex((a) => a === "--account" || a === "-a");
69
+ if (accIdx !== -1) {
70
+ accountFlag = rawArgs[accIdx + 1];
71
+ rawArgs.splice(accIdx, accountFlag ? 2 : 1);
72
+ }
73
+
55
74
  const args = rawArgs.filter((a) => !a.startsWith("-"));
56
75
  const [command, ...rest] = args;
57
76
 
@@ -59,6 +78,14 @@ if (verbose) {
59
78
  setVerbose(true);
60
79
  }
61
80
 
81
+ // fold a pre-multi-account session.txt into account "default" (one-time, no-op after)
82
+ migrateLegacyIfNeeded();
83
+
84
+ // an explicit --account pins this invocation to that account's session
85
+ if (accountFlag) {
86
+ setSessionPath(accountSessionPath(accountFlag));
87
+ }
88
+
62
89
  async function main() {
63
90
  switch (command) {
64
91
  case "send":
@@ -110,7 +137,23 @@ async function main() {
110
137
  break;
111
138
 
112
139
  case "login":
113
- await login();
140
+ await login(rest[0]);
141
+ break;
142
+
143
+ case "accounts":
144
+ await accounts();
145
+ break;
146
+
147
+ case "switch":
148
+ await switchAccount(rest[0]);
149
+ break;
150
+
151
+ case "whoami":
152
+ await whoami();
153
+ break;
154
+
155
+ case "logout":
156
+ await logout(rest[0]);
114
157
  break;
115
158
 
116
159
  case "config":
@@ -128,7 +171,10 @@ async function main() {
128
171
  }
129
172
  }
130
173
 
131
- main().catch((err) => {
132
- console.error("error:", err.message);
174
+ function die(err: unknown) {
175
+ console.error("error:", explainConnectionError(err).message);
133
176
  process.exit(1);
134
- });
177
+ }
178
+
179
+ process.on("unhandledRejection", die);
180
+ main().catch(die);
@@ -4,6 +4,7 @@ import { Logger } from "telegram/extensions/Logger";
4
4
  import type { LogLevel } from "telegram/extensions/Logger";
5
5
  import { loadSession, saveSession } from "../session/storage";
6
6
  import { getApiCredentials } from "../config/manager";
7
+ import { wssEnabled, wssClientParams, applyWss, restoreTcpDc, explainConnectionError } from "./wss";
7
8
 
8
9
  let verbose = false;
9
10
 
@@ -46,9 +47,17 @@ export async function getClient(): Promise<TelegramClient> {
46
47
  client = new TelegramClient(session, creds.appId, creds.appHash, {
47
48
  connectionRetries: 5,
48
49
  baseLogger: new SilentLogger(),
50
+ ...(wssEnabled() ? wssClientParams : {}),
49
51
  });
50
52
 
51
- await client.connect();
53
+ if (wssEnabled()) applyWss(client);
54
+ else restoreTcpDc(client);
55
+
56
+ try {
57
+ await client.connect();
58
+ } catch (err) {
59
+ throw explainConnectionError(err);
60
+ }
52
61
  return client;
53
62
  }
54
63
 
@@ -0,0 +1,162 @@
1
+ import type { TelegramClient } from "telegram";
2
+ import { ConnectionTCPObfuscated } from "telegram/network";
3
+
4
+ import { wssEnabled } from "../config/manager";
5
+ export { wssEnabled };
6
+
7
+ const WEB_DC_HOSTS: Record<number, string> = {
8
+ 1: "pluto",
9
+ 2: "venus",
10
+ 3: "aurora",
11
+ 4: "vesta",
12
+ 5: "flora",
13
+ };
14
+
15
+ const TCP_DC_IPS: Record<number, string> = {
16
+ 1: "149.154.175.53",
17
+ 2: "149.154.167.51",
18
+ 3: "149.154.175.100",
19
+ 4: "149.154.167.91",
20
+ 5: "91.108.56.130",
21
+ };
22
+
23
+ export function isWebHost(host: string): boolean {
24
+ return host.endsWith(".web.telegram.org");
25
+ }
26
+
27
+ export function restoreTcpDc(client: TelegramClient): void {
28
+ const host = client.session.serverAddress;
29
+ if (!host || !isWebHost(host)) return;
30
+ const dcId = client.session.dcId;
31
+ const ip = TCP_DC_IPS[dcId];
32
+ if (!ip) throw new Error(`no tcp ip for DC ${dcId}`);
33
+ client.session.setDC(dcId, ip, 443);
34
+ }
35
+
36
+ export const MTPROTO_BLOCKED_HINT =
37
+ "mtproto handshake failed — this network likely blocks raw telegram DC traffic.\n" +
38
+ "retry over websockets: TELEGRAM_WSS=1 or `telegram config set wss true`";
39
+
40
+ export function explainConnectionError(err: unknown): Error {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ if (!wssEnabled() && msg.includes("Not connected")) {
43
+ return new Error(`${MTPROTO_BLOCKED_HINT}\noriginal: ${msg}`);
44
+ }
45
+ return err instanceof Error ? err : new Error(msg);
46
+ }
47
+
48
+ export function webDcHost(dcId: number, download = false): string {
49
+ const name = WEB_DC_HOSTS[dcId];
50
+ if (!name) throw new Error(`no web host for DC ${dcId}`);
51
+ return `${name}${download ? "-1" : ""}.web.telegram.org`;
52
+ }
53
+
54
+ const closeError = new Error("WebSocket was closed");
55
+
56
+ // gramjs ships PromisedWebSockets on top of the `websocket` npm package,
57
+ // which breaks under bun (101 upgrade surfaces as a plain http response).
58
+ // native WebSocket exists in bun and node >= 22, so use that instead.
59
+ // same read/write shape gramjs' Connection expects.
60
+ class NativeWebSocket {
61
+ private ws?: WebSocket;
62
+ private stream = Buffer.alloc(0);
63
+ private closed = true;
64
+ private canRead!: Promise<boolean>;
65
+ private resolveRead?: (value: boolean) => void;
66
+
67
+ private resetRead() {
68
+ this.canRead = new Promise((resolve) => {
69
+ this.resolveRead = resolve;
70
+ });
71
+ }
72
+
73
+ getWebSocketLink(host: string, port: number, testServers: boolean): string {
74
+ const scheme = port === 443 ? "wss" : "ws";
75
+ return `${scheme}://${host}/apiws${testServers ? "_test" : ""}`;
76
+ }
77
+
78
+ async connect(port: number, host: string, testServers = false): Promise<this> {
79
+ if (typeof WebSocket === "undefined") {
80
+ throw new Error("wss transport needs a native WebSocket (bun or node >= 22)");
81
+ }
82
+ this.stream = Buffer.alloc(0);
83
+ this.resetRead();
84
+ this.closed = false;
85
+ const url = this.getWebSocketLink(host, port, testServers);
86
+ const ws = new WebSocket(url, "binary");
87
+ ws.binaryType = "arraybuffer";
88
+ this.ws = ws;
89
+
90
+ return new Promise((resolve, reject) => {
91
+ ws.onopen = () => resolve(this);
92
+ ws.onerror = (event) => {
93
+ reject(new Error(`wss connect to ${url} failed: ${(event as ErrorEvent).message ?? "unknown"}`));
94
+ };
95
+ ws.onclose = () => {
96
+ this.closed = true;
97
+ this.resolveRead?.(false);
98
+ };
99
+ ws.onmessage = (message) => {
100
+ this.stream = Buffer.concat([this.stream, Buffer.from(message.data as ArrayBuffer)]);
101
+ this.resolveRead?.(true);
102
+ };
103
+ });
104
+ }
105
+
106
+ async read(number: number): Promise<Buffer> {
107
+ if (this.closed) throw closeError;
108
+ await this.canRead;
109
+ if (this.closed) throw closeError;
110
+ const toReturn = this.stream.subarray(0, number);
111
+ this.stream = this.stream.subarray(number);
112
+ if (this.stream.length === 0) this.resetRead();
113
+ return toReturn;
114
+ }
115
+
116
+ async readExactly(number: number): Promise<Buffer> {
117
+ let readData = Buffer.alloc(0);
118
+ while (number > 0) {
119
+ const chunk = await this.read(number);
120
+ readData = Buffer.concat([readData, chunk]);
121
+ number -= chunk.length;
122
+ }
123
+ return readData;
124
+ }
125
+
126
+ async readAll(): Promise<Buffer> {
127
+ if (this.closed || !(await this.canRead)) throw closeError;
128
+ const toReturn = this.stream;
129
+ this.stream = Buffer.alloc(0);
130
+ this.resetRead();
131
+ return toReturn;
132
+ }
133
+
134
+ write(data: Buffer) {
135
+ if (this.closed) throw closeError;
136
+ this.ws?.send(data);
137
+ }
138
+
139
+ async close() {
140
+ this.ws?.close();
141
+ this.closed = true;
142
+ }
143
+
144
+ toString() {
145
+ return "NativeWebSocket";
146
+ }
147
+ }
148
+
149
+ export const wssClientParams = {
150
+ useWSS: true,
151
+ networkSocket: NativeWebSocket as any,
152
+ connection: ConnectionTCPObfuscated,
153
+ } as const;
154
+
155
+ export function applyWss(client: TelegramClient): void {
156
+ const dcId = client.session.dcId || 2;
157
+ client.session.setDC(dcId, webDcHost(dcId), 443);
158
+
159
+ const original = client.getDC.bind(client);
160
+ client.getDC = (id: number, downloadDC = false) =>
161
+ original(id, downloadDC, true);
162
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ copyFileSync,
7
+ rmSync,
8
+ } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ // kept in sync with manager.ts; recomputed here to avoid a circular import
13
+ const CONFIG_DIR = join(homedir(), ".supertelegram");
14
+ const ACCOUNTS_DIR = join(CONFIG_DIR, "accounts");
15
+ const ACCOUNTS_FILE = join(CONFIG_DIR, "accounts.json");
16
+ const LEGACY_SESSION_FILE = join(CONFIG_DIR, "session.txt");
17
+
18
+ export interface AccountMeta {
19
+ username?: string;
20
+ userId?: string;
21
+ name?: string;
22
+ }
23
+
24
+ export interface AccountsRegistry {
25
+ current?: string;
26
+ accounts: Record<string, AccountMeta>;
27
+ }
28
+
29
+ function readRegistry(): AccountsRegistry {
30
+ if (!existsSync(ACCOUNTS_FILE)) return { accounts: {} };
31
+ try {
32
+ return JSON.parse(readFileSync(ACCOUNTS_FILE, "utf-8"));
33
+ } catch {
34
+ return { accounts: {} };
35
+ }
36
+ }
37
+
38
+ function writeRegistry(reg: AccountsRegistry): void {
39
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
40
+ writeFileSync(ACCOUNTS_FILE, JSON.stringify(reg, null, 2));
41
+ }
42
+
43
+ export function accountSessionPath(name: string): string {
44
+ return join(ACCOUNTS_DIR, `${name}.txt`);
45
+ }
46
+
47
+ export function getCurrentAccount(): string | undefined {
48
+ return readRegistry().current;
49
+ }
50
+
51
+ export function listAccounts(): {
52
+ name: string;
53
+ meta: AccountMeta;
54
+ current: boolean;
55
+ }[] {
56
+ const reg = readRegistry();
57
+ return Object.entries(reg.accounts).map(([name, meta]) => ({
58
+ name,
59
+ meta,
60
+ current: name === reg.current,
61
+ }));
62
+ }
63
+
64
+ export function setCurrentAccount(name: string): void {
65
+ const reg = readRegistry();
66
+ if (!reg.accounts[name]) {
67
+ const known = Object.keys(reg.accounts);
68
+ throw new Error(
69
+ `account "${name}" not found.` +
70
+ (known.length ? ` known accounts: ${known.join(", ")}` : " run: telegram login <name>")
71
+ );
72
+ }
73
+ reg.current = name;
74
+ writeRegistry(reg);
75
+ }
76
+
77
+ export function registerAccount(
78
+ name: string,
79
+ meta: AccountMeta,
80
+ makeCurrent = true
81
+ ): void {
82
+ if (!existsSync(ACCOUNTS_DIR)) mkdirSync(ACCOUNTS_DIR, { recursive: true });
83
+ const reg = readRegistry();
84
+ reg.accounts[name] = meta;
85
+ if (makeCurrent) reg.current = name;
86
+ writeRegistry(reg);
87
+ }
88
+
89
+ export function removeAccount(name: string): void {
90
+ const reg = readRegistry();
91
+ if (!reg.accounts[name]) {
92
+ throw new Error(`account "${name}" not found`);
93
+ }
94
+ delete reg.accounts[name];
95
+ if (reg.current === name) reg.current = Object.keys(reg.accounts)[0];
96
+ writeRegistry(reg);
97
+
98
+ const sessionFile = accountSessionPath(name);
99
+ if (existsSync(sessionFile)) rmSync(sessionFile);
100
+ }
101
+
102
+ // one-time migration: fold a pre-multi-account session.txt into account "default"
103
+ // so existing installs keep their login after upgrading.
104
+ export function migrateLegacyIfNeeded(): void {
105
+ const reg = readRegistry();
106
+ if (Object.keys(reg.accounts).length > 0) return; // already on the accounts system
107
+
108
+ if (existsSync(LEGACY_SESSION_FILE) && readFileSync(LEGACY_SESSION_FILE, "utf-8").trim()) {
109
+ if (!existsSync(ACCOUNTS_DIR)) mkdirSync(ACCOUNTS_DIR, { recursive: true });
110
+ copyFileSync(LEGACY_SESSION_FILE, accountSessionPath("default"));
111
+ registerAccount("default", {}, true);
112
+ }
113
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { getCurrentAccount, accountSessionPath } from "./accounts";
4
5
 
5
6
  const CONFIG_DIR = join(homedir(), ".supertelegram");
6
7
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
@@ -9,6 +10,7 @@ const SESSION_FILE = join(CONFIG_DIR, "session.txt");
9
10
  export interface Config {
10
11
  appId?: string;
11
12
  appHash?: string;
13
+ wss?: string;
12
14
  }
13
15
 
14
16
  function ensureConfigDir() {
@@ -44,27 +46,39 @@ export function setConfig(key: string, value: string) {
44
46
 
45
47
  export function getSessionPath(customPath?: string): string {
46
48
  // precedence:
47
- // 1. custom path from flag
49
+ // 1. custom path from flag (--account resolves to one of these)
48
50
  // 2. TELEGRAM_SESSION env var
49
- // 3. global ~/.supertelegram/session.txt
51
+ // 3. current account's session (~/.supertelegram/accounts/<name>.txt)
50
52
  // 4. local ./session.txt (backwards compat)
51
-
53
+ // 5. global ~/.supertelegram/session.txt (legacy default)
54
+
52
55
  if (customPath) {
53
56
  return customPath;
54
57
  }
55
-
58
+
56
59
  if (process.env.TELEGRAM_SESSION) {
57
60
  return process.env.TELEGRAM_SESSION;
58
61
  }
59
-
62
+
63
+ const current = getCurrentAccount();
64
+ if (current) {
65
+ return accountSessionPath(current);
66
+ }
67
+
60
68
  // check if local session.txt exists (backwards compat)
61
69
  if (existsSync("./session.txt")) {
62
70
  return "./session.txt";
63
71
  }
64
-
72
+
65
73
  return SESSION_FILE;
66
74
  }
67
75
 
76
+ export function wssEnabled(): boolean {
77
+ const env = process.env.TELEGRAM_WSS;
78
+ if (env !== undefined) return env === "1" || env === "true";
79
+ return getConfig().wss === "true";
80
+ }
81
+
68
82
  export function getApiCredentials(): { appId: number; appHash: string } | null {
69
83
  // precedence:
70
84
  // 1. env vars