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 +16 -0
- package/package.json +1 -1
- package/src/cli/commands.ts +4 -1
- package/src/cli/run.ts +8 -4
- package/src/client/telegram.ts +10 -1
- package/src/client/wss.ts +162 -0
- package/src/config/manager.ts +7 -0
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
package/src/cli/commands.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/client/telegram.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/config/manager.ts
CHANGED
|
@@ -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
|