@emaxe/tuigram 1.0.0 → 1.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/.env.example +17 -0
- package/CHANGELOG.md +117 -0
- package/CHANGELOG.ru.md +119 -0
- package/README.md +527 -199
- package/README.ru.md +687 -0
- package/package.json +4 -1
- package/src/cli/init.js +2 -1
- package/src/config.js +125 -0
- package/src/telegram/client.js +15 -7
- package/src/telegram/socket.js +224 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emaxe/tuigram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Полнофункциональный TUI & CLI клиент Telegram на Node.js на базе MTProto (teleproto)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"bin/",
|
|
14
14
|
"src/",
|
|
15
15
|
"README.md",
|
|
16
|
+
"README.ru.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"CHANGELOG.ru.md",
|
|
16
19
|
"LICENSE",
|
|
17
20
|
".env.example"
|
|
18
21
|
],
|
package/src/cli/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import input from "input";
|
|
4
4
|
import { bold, cyan, green, yellow, dim } from "colorette";
|
|
5
|
-
import { config } from "../config.js";
|
|
5
|
+
import { config, formatProxyUrl } from "../config.js";
|
|
6
6
|
|
|
7
7
|
/** Сколько раз переспрашивать значение, прежде чем сдаться. */
|
|
8
8
|
const MAX_ATTEMPTS = 3;
|
|
@@ -193,5 +193,6 @@ export function cmdPaths() {
|
|
|
193
193
|
console.log(` Сессия: ${config.sessionPath} ${sessionExists ? green("(есть)") : dim("(нет)")}`);
|
|
194
194
|
console.log(` Загрузки: ${config.downloadsDir}`);
|
|
195
195
|
console.log(` Ключи API: ${config.apiId && config.apiHash ? green("заданы") : yellow("не заданы — запустите tuigram init")}`);
|
|
196
|
+
console.log(` Прокси: ${config.proxy ? green(formatProxyUrl(config.proxy)) : dim("не используется")}`);
|
|
196
197
|
console.log("");
|
|
197
198
|
}
|
package/src/config.js
CHANGED
|
@@ -110,6 +110,129 @@ function readVersion() {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {Object} ProxyConfig
|
|
115
|
+
* @property {"http"|"https"|"socks5"|"socks4"} type - Протокол прокси
|
|
116
|
+
* @property {string} host - Адрес сервера прокси
|
|
117
|
+
* @property {number} port - Порт сервера прокси
|
|
118
|
+
* @property {string} [username] - Логин пользователя
|
|
119
|
+
* @property {string} [password] - Пароль пользователя
|
|
120
|
+
* @property {number} [timeout] - Таймаут в секундах
|
|
121
|
+
* @property {string} ip - Хост прокси (для совместимости с MTProto)
|
|
122
|
+
* @property {4|5} [socksType] - Тип SOCKS-прокси
|
|
123
|
+
* @property {boolean} [http] - Флаг HTTP-прокси
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Нормализует тип прокси из строки протокола.
|
|
128
|
+
* @param {string} [raw]
|
|
129
|
+
* @returns {"http"|"https"|"socks5"|"socks4"}
|
|
130
|
+
*/
|
|
131
|
+
function normalizeProxyType(raw) {
|
|
132
|
+
const clean = String(raw || "").toLowerCase().replace(/:$/, "").trim();
|
|
133
|
+
if (clean === "https") return "https";
|
|
134
|
+
if (clean === "socks4" || clean === "socks4a") return "socks4";
|
|
135
|
+
if (clean === "socks5" || clean === "socks5h" || clean === "socks") return "socks5";
|
|
136
|
+
return "http";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Разбирает параметры прокси из переменных окружения или переданного объекта.
|
|
141
|
+
* Поддерживает URL-формат (PROXY_URL, HTTPS_PROXY, HTTP_PROXY, ALL_PROXY)
|
|
142
|
+
* и отдельные переменные (PROXY_TYPE, PROXY_HOST, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD).
|
|
143
|
+
* @param {Record<string, string|undefined>} [env=process.env]
|
|
144
|
+
* @returns {ProxyConfig|null}
|
|
145
|
+
*/
|
|
146
|
+
export function parseProxyConfig(env = process.env) {
|
|
147
|
+
const rawUrl = env.PROXY_URL || env.proxy_url || (!env.PROXY_HOST && !env.PROXY_IP && !env.proxy_host && !env.proxy_ip
|
|
148
|
+
? (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy)
|
|
149
|
+
: undefined);
|
|
150
|
+
|
|
151
|
+
let type = "http";
|
|
152
|
+
let host = "";
|
|
153
|
+
let port = 0;
|
|
154
|
+
let username = "";
|
|
155
|
+
let password = "";
|
|
156
|
+
|
|
157
|
+
if (rawUrl && typeof rawUrl === "string" && rawUrl.trim()) {
|
|
158
|
+
let urlStr = rawUrl.trim();
|
|
159
|
+
// Добавляем схему по умолчанию, если передан host:port без префикса
|
|
160
|
+
if (!/^[a-zA-Z0-9+-.]+:\/\//.test(urlStr)) {
|
|
161
|
+
const defaultScheme = (env.PROXY_TYPE || "http").toLowerCase().replace(/:$/, "");
|
|
162
|
+
urlStr = `${defaultScheme}://${urlStr}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const parsed = new URL(urlStr);
|
|
167
|
+
type = normalizeProxyType(parsed.protocol);
|
|
168
|
+
host = parsed.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
169
|
+
port = parsed.port ? parseInt(parsed.port, 10) : (type === "https" ? 443 : (type.startsWith("socks") ? 1080 : 8080));
|
|
170
|
+
if (parsed.username) username = decodeURIComponent(parsed.username);
|
|
171
|
+
if (parsed.password) password = decodeURIComponent(parsed.password);
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
} else if (env.PROXY_HOST || env.PROXY_IP) {
|
|
176
|
+
type = normalizeProxyType(env.PROXY_TYPE || "http");
|
|
177
|
+
host = String(env.PROXY_HOST || env.PROXY_IP).trim();
|
|
178
|
+
const defaultPort = type === "https" ? 443 : (type.startsWith("socks") ? 1080 : 8080);
|
|
179
|
+
port = env.PROXY_PORT ? parseInt(String(env.PROXY_PORT).trim(), 10) : defaultPort;
|
|
180
|
+
username = String(env.PROXY_USERNAME || env.PROXY_USER || "").trim();
|
|
181
|
+
password = String(env.PROXY_PASSWORD || env.PROXY_PASS || "").trim();
|
|
182
|
+
} else {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!host || !port || isNaN(port) || port <= 0 || port > 65535) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rawTimeout = env.PROXY_TIMEOUT ? parseInt(String(env.PROXY_TIMEOUT).trim(), 10) : 10;
|
|
191
|
+
const timeout = isNaN(rawTimeout) || rawTimeout <= 0 ? 10 : rawTimeout;
|
|
192
|
+
|
|
193
|
+
/** @type {ProxyConfig} */
|
|
194
|
+
const result = {
|
|
195
|
+
type,
|
|
196
|
+
host,
|
|
197
|
+
port,
|
|
198
|
+
ip: host,
|
|
199
|
+
timeout,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (username) result.username = username;
|
|
203
|
+
if (password) result.password = password;
|
|
204
|
+
|
|
205
|
+
if (type === "socks5") {
|
|
206
|
+
result.socksType = 5;
|
|
207
|
+
} else if (type === "socks4") {
|
|
208
|
+
result.socksType = 4;
|
|
209
|
+
} else {
|
|
210
|
+
result.http = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Форматирует конфигурацию прокси в строку для вывода в терминал.
|
|
218
|
+
* Пароль маскируется звездочками в целях безопасности.
|
|
219
|
+
* @param {ProxyConfig|null} proxy
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
export function formatProxyUrl(proxy) {
|
|
223
|
+
if (!proxy) return "не используется";
|
|
224
|
+
|
|
225
|
+
let auth = "";
|
|
226
|
+
if (proxy.username) {
|
|
227
|
+
auth = proxy.password
|
|
228
|
+
? `${encodeURIComponent(proxy.username)}:***@`
|
|
229
|
+
: `${encodeURIComponent(proxy.username)}@`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const host = proxy.host.includes(":") ? `[${proxy.host}]` : proxy.host;
|
|
233
|
+
return `${proxy.type}://${auth}${host}:${proxy.port}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
113
236
|
export const config = {
|
|
114
237
|
packageRoot,
|
|
115
238
|
/** @deprecated оставлено для обратной совместимости, равно packageRoot */
|
|
@@ -129,6 +252,8 @@ export const config = {
|
|
|
129
252
|
autoScroll: String(process.env.AUTO_SCROLL || "true").toLowerCase() === "true",
|
|
130
253
|
showTyping: String(process.env.SHOW_TYPING || "true").toLowerCase() === "true",
|
|
131
254
|
|
|
255
|
+
proxy: parseProxyConfig(),
|
|
256
|
+
|
|
132
257
|
/**
|
|
133
258
|
* Путь к файлу сессии.
|
|
134
259
|
* @returns {string}
|
package/src/telegram/client.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StringSession } from "teleproto/sessions/index.js";
|
|
|
4
4
|
import { Logger, LogLevel } from "teleproto/extensions/Logger.js";
|
|
5
5
|
import { config } from "../config.js";
|
|
6
6
|
import { saveSessionFile, readFileSafe } from "../utils/storage.js";
|
|
7
|
+
import { TuiGramNetSockets } from "./socket.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Читает сохранённую строку сессии.
|
|
@@ -42,17 +43,24 @@ export function clearSession() {
|
|
|
42
43
|
export function buildClient(sessionString = readSession()) {
|
|
43
44
|
config.assertCredentials();
|
|
44
45
|
|
|
46
|
+
const clientParams = {
|
|
47
|
+
connectionRetries: 10,
|
|
48
|
+
autoReconnect: true,
|
|
49
|
+
retryDelay: 1500,
|
|
50
|
+
baseLogger: new Logger(LogLevel.ERROR),
|
|
51
|
+
useWSS: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (config.proxy) {
|
|
55
|
+
clientParams.proxy = config.proxy;
|
|
56
|
+
clientParams.networkSocket = TuiGramNetSockets;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
const client = new TelegramClient(
|
|
46
60
|
new StringSession(sessionString),
|
|
47
61
|
config.apiId,
|
|
48
62
|
config.apiHash,
|
|
49
|
-
|
|
50
|
-
connectionRetries: 10,
|
|
51
|
-
autoReconnect: true,
|
|
52
|
-
retryDelay: 1500,
|
|
53
|
-
baseLogger: new Logger(LogLevel.ERROR),
|
|
54
|
-
useWSS: false,
|
|
55
|
-
}
|
|
63
|
+
clientParams
|
|
56
64
|
);
|
|
57
65
|
|
|
58
66
|
return client;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сетевой транспорт для MTProto с поддержкой прокси (HTTP CONNECT, SOCKS5, SOCKS4).
|
|
3
|
+
*/
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import https from "node:https";
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import { PromisedNetSockets } from "teleproto/extensions/PromisedNetSockets.js";
|
|
8
|
+
import { SocksClient } from "socks";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Создаёт TCP-туннель через HTTP/HTTPS прокси методом HTTP CONNECT.
|
|
12
|
+
* Поддерживает как прокси с Basic-авторизацией, так и без неё.
|
|
13
|
+
* @param {object} params
|
|
14
|
+
* @param {boolean} [params.isHttps=false] подключение к прокси-серверу по TLS
|
|
15
|
+
* @param {string} params.proxyHost адрес прокси-сервера
|
|
16
|
+
* @param {number} params.proxyPort порт прокси-сервера
|
|
17
|
+
* @param {string} params.targetHost адрес целевого сервера Telegram MTProto
|
|
18
|
+
* @param {number} params.targetPort порт целевого сервера Telegram MTProto
|
|
19
|
+
* @param {string} [params.username] логин для авторизации на прокси
|
|
20
|
+
* @param {string} [params.password] пароль для авторизации на прокси
|
|
21
|
+
* @param {number} [params.timeout=10000] таймаут установки соединения в миллисекундах
|
|
22
|
+
* @returns {Promise<import("node:net").Socket>}
|
|
23
|
+
*/
|
|
24
|
+
export function createHttpConnectSocket({
|
|
25
|
+
isHttps = false,
|
|
26
|
+
proxyHost,
|
|
27
|
+
proxyPort,
|
|
28
|
+
targetHost,
|
|
29
|
+
targetPort,
|
|
30
|
+
username,
|
|
31
|
+
password,
|
|
32
|
+
timeout = 10000,
|
|
33
|
+
}) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const clientModule = isHttps ? https : http;
|
|
36
|
+
|
|
37
|
+
const headers = {
|
|
38
|
+
Host: `${targetHost}:${targetPort}`,
|
|
39
|
+
"User-Agent": "TuiGram",
|
|
40
|
+
"Proxy-Connection": "Keep-Alive",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (username || password) {
|
|
44
|
+
const auth = Buffer.from(`${username || ""}:${password || ""}`).toString("base64");
|
|
45
|
+
headers["Proxy-Authorization"] = `Basic ${auth}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const req = clientModule.request({
|
|
49
|
+
host: proxyHost,
|
|
50
|
+
port: proxyPort,
|
|
51
|
+
method: "CONNECT",
|
|
52
|
+
path: `${targetHost}:${targetPort}`,
|
|
53
|
+
headers,
|
|
54
|
+
timeout,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let finished = false;
|
|
58
|
+
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
req.removeAllListeners();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
req.on("connect", (res, socket, head) => {
|
|
64
|
+
if (finished) return;
|
|
65
|
+
finished = true;
|
|
66
|
+
cleanup();
|
|
67
|
+
|
|
68
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
69
|
+
// Если в заголовке ответа уже были данные следующего протокола, возвращаем их в сокет
|
|
70
|
+
if (head && head.length > 0) {
|
|
71
|
+
socket.unshift(head);
|
|
72
|
+
}
|
|
73
|
+
resolve(socket);
|
|
74
|
+
} else if (res.statusCode === 407) {
|
|
75
|
+
socket.destroy();
|
|
76
|
+
reject(new Error("Ошибка авторизации на HTTP-прокси (407 Proxy Authentication Required)"));
|
|
77
|
+
} else {
|
|
78
|
+
socket.destroy();
|
|
79
|
+
reject(new Error(`HTTP-прокси вернул код ошибки: ${res.statusCode} ${res.statusMessage || ""}`.trim()));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
req.on("response", (res) => {
|
|
84
|
+
if (finished) return;
|
|
85
|
+
finished = true;
|
|
86
|
+
cleanup();
|
|
87
|
+
if (res.statusCode === 407) {
|
|
88
|
+
reject(new Error("Ошибка авторизации на HTTP-прокси (407 Proxy Authentication Required)"));
|
|
89
|
+
} else {
|
|
90
|
+
reject(new Error(`HTTP-прокси вернул код ошибки: ${res.statusCode} ${res.statusMessage || ""}`.trim()));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.on("timeout", () => {
|
|
95
|
+
if (finished) return;
|
|
96
|
+
finished = true;
|
|
97
|
+
cleanup();
|
|
98
|
+
req.destroy(new Error(`Таймаут подключения к HTTP-прокси (${proxyHost}:${proxyPort})`));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.on("error", (err) => {
|
|
102
|
+
if (finished) return;
|
|
103
|
+
finished = true;
|
|
104
|
+
cleanup();
|
|
105
|
+
reject(new Error(`Не удалось подключиться к HTTP-прокси (${proxyHost}:${proxyPort}): ${err.message}`));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
req.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Расширенный сокет для teleproto с поддержкой HTTP CONNECT и SOCKS5/4 туннелирования.
|
|
114
|
+
*/
|
|
115
|
+
export class TuiGramNetSockets extends PromisedNetSockets {
|
|
116
|
+
/**
|
|
117
|
+
* @param {object} [proxy]
|
|
118
|
+
* @param {number} [keepAliveInterval]
|
|
119
|
+
*/
|
|
120
|
+
constructor(proxy, keepAliveInterval) {
|
|
121
|
+
// Передаём undefined в базовый класс, чтобы обойти валидацию socksType для HTTP-прокси
|
|
122
|
+
super(undefined, keepAliveInterval);
|
|
123
|
+
this.proxy = proxy;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Устанавливает сетевое соединение с дата-центром Telegram.
|
|
128
|
+
* @param {number} port
|
|
129
|
+
* @param {string} ip
|
|
130
|
+
* @returns {Promise<this>}
|
|
131
|
+
*/
|
|
132
|
+
async connect(port, ip) {
|
|
133
|
+
this.chunks = [];
|
|
134
|
+
this.headOffset = 0;
|
|
135
|
+
this.available = 0;
|
|
136
|
+
let connected = false;
|
|
137
|
+
|
|
138
|
+
if (this.proxy) {
|
|
139
|
+
const proxyType = (this.proxy.type || "").toLowerCase();
|
|
140
|
+
const host = this.proxy.host || this.proxy.ip;
|
|
141
|
+
const proxyPort = Number(this.proxy.port);
|
|
142
|
+
const timeout = (this.proxy.timeout || 10) * 1000;
|
|
143
|
+
|
|
144
|
+
if (proxyType === "socks5" || proxyType === "socks4" || this.proxy.socksType) {
|
|
145
|
+
const socksType = this.proxy.socksType || (proxyType === "socks4" ? 4 : 5);
|
|
146
|
+
const info = await SocksClient.createConnection({
|
|
147
|
+
proxy: {
|
|
148
|
+
host,
|
|
149
|
+
port: proxyPort,
|
|
150
|
+
type: socksType,
|
|
151
|
+
userId: this.proxy.username,
|
|
152
|
+
password: this.proxy.password,
|
|
153
|
+
},
|
|
154
|
+
command: "connect",
|
|
155
|
+
timeout,
|
|
156
|
+
destination: {
|
|
157
|
+
host: ip,
|
|
158
|
+
port: port,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
this.client = info.socket;
|
|
162
|
+
connected = true;
|
|
163
|
+
} else if (proxyType === "http" || proxyType === "https" || this.proxy.http) {
|
|
164
|
+
this.client = await createHttpConnectSocket({
|
|
165
|
+
isHttps: proxyType === "https",
|
|
166
|
+
proxyHost: host,
|
|
167
|
+
proxyPort: proxyPort,
|
|
168
|
+
targetHost: ip,
|
|
169
|
+
targetPort: port,
|
|
170
|
+
username: this.proxy.username,
|
|
171
|
+
password: this.proxy.password,
|
|
172
|
+
timeout,
|
|
173
|
+
});
|
|
174
|
+
connected = true;
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error(`Неподдерживаемый тип прокси: ${this.proxy.type}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
this.client = new net.Socket();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.canRead = new Promise((resolve) => {
|
|
183
|
+
this.resolveRead = resolve;
|
|
184
|
+
});
|
|
185
|
+
this.closed = false;
|
|
186
|
+
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
if (!this.client) {
|
|
189
|
+
return reject(new Error("Сетевой сокет не инициализирован"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const tune = (socket) => {
|
|
193
|
+
socket.setNoDelay(true);
|
|
194
|
+
socket.setKeepAlive(this.keepAliveInterval > 0, Math.max(0, this.keepAliveInterval));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (connected) {
|
|
198
|
+
tune(this.client);
|
|
199
|
+
this.receive();
|
|
200
|
+
resolve(this);
|
|
201
|
+
} else {
|
|
202
|
+
this.client.connect(port, ip, () => {
|
|
203
|
+
tune(this.client);
|
|
204
|
+
this.receive();
|
|
205
|
+
resolve(this);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.client.on("error", reject);
|
|
210
|
+
this.client.on("close", () => {
|
|
211
|
+
if (this.client && this.client.destroyed) {
|
|
212
|
+
if (this.resolveRead) {
|
|
213
|
+
this.resolveRead(false);
|
|
214
|
+
}
|
|
215
|
+
this.closed = true;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
toString() {
|
|
222
|
+
return "TuiGramNetSocket";
|
|
223
|
+
}
|
|
224
|
+
}
|