supertelegram 0.5.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.
package/README.md CHANGED
@@ -128,6 +128,22 @@ TELEGRAM_SESSION=./custom.txt telegram send @friend "hey"
128
128
  4. `./session.txt` (backwards compat)
129
129
  5. `~/.supertelegram/session.txt` (legacy default)
130
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).
146
+
131
147
  ## development
132
148
 
133
149
  clone repo and install:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supertelegram",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "telegram cli for humans and bots",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -10,7 +10,7 @@ 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
14
  import {
15
15
  listAccounts,
16
16
  setCurrentAccount,
@@ -294,6 +294,8 @@ export async function config(action?: string, key?: string, value?: string) {
294
294
  console.log(cfg.appId);
295
295
  } else if (key === "appHash" && cfg) {
296
296
  console.log(cfg.appHash);
297
+ } else if (key === "wss") {
298
+ console.log(String(getConfig().wss === "true"));
297
299
  } else {
298
300
  console.log("not found");
299
301
  }
@@ -303,6 +305,7 @@ export async function config(action?: string, key?: string, value?: string) {
303
305
  console.log("usage:");
304
306
  console.log(" telegram config set appId <id>");
305
307
  console.log(" telegram config set appHash <hash>");
308
+ console.log(" telegram config set wss true (websocket transport for networks blocking mtproto)");
306
309
  console.log(" telegram config get appId");
307
310
  }
308
311
 
package/src/cli/run.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { explainConnectionError } from "../client/wss";
2
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";
4
5
  import { migrateLegacyIfNeeded, accountSessionPath } from "../config/accounts";
@@ -26,7 +27,7 @@ commands:
26
27
  switch <name> switch the active account
27
28
  whoami show the active account
28
29
  logout [name] remove an account (default: current)
29
- config set <key> <val> set API credentials (appId, appHash)
30
+ config set <key> <val> set API credentials (appId, appHash) or wss true
30
31
 
31
32
  options:
32
33
  -a, --account <name> run this command as a specific account
@@ -170,7 +171,10 @@ async function main() {
170
171
  }
171
172
  }
172
173
 
173
- main().catch((err) => {
174
- console.error("error:", err.message);
174
+ function die(err: unknown) {
175
+ console.error("error:", explainConnectionError(err).message);
175
176
  process.exit(1);
176
- });
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
+ }
@@ -10,6 +10,7 @@ const SESSION_FILE = join(CONFIG_DIR, "session.txt");
10
10
  export interface Config {
11
11
  appId?: string;
12
12
  appHash?: string;
13
+ wss?: string;
13
14
  }
14
15
 
15
16
  function ensureConfigDir() {
@@ -72,6 +73,12 @@ export function getSessionPath(customPath?: string): string {
72
73
  return SESSION_FILE;
73
74
  }
74
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
+
75
82
  export function getApiCredentials(): { appId: number; appHash: string } | null {
76
83
  // precedence:
77
84
  // 1. env vars