webmaxsocket 1.0.0 → 1.1.1
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 +300 -16
- package/config/example.json +6 -0
- package/example-ios.js +186 -0
- package/example-sms.js +131 -0
- package/example-token.js +100 -0
- package/example.js +2 -0
- package/index.js +2 -0
- package/lib/client.js +510 -54
- package/lib/opcodes.js +3 -1
- package/lib/socketTransport.js +296 -0
- package/lib/userAgent.js +8 -4
- package/package.json +16 -3
package/lib/client.js
CHANGED
|
@@ -1,13 +1,59 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const WebSocket = require('ws');
|
|
2
4
|
const EventEmitter = require('events');
|
|
3
5
|
const { v4: uuidv4 } = require('uuid');
|
|
4
6
|
const qrcode = require('qrcode-terminal');
|
|
5
7
|
const SessionManager = require('./session');
|
|
8
|
+
const { MaxSocketTransport } = require('./socketTransport');
|
|
6
9
|
const { Message, ChatAction, User } = require('./entities');
|
|
7
10
|
const { EventTypes, ChatActions } = require('./constants');
|
|
8
11
|
const { Opcode, DeviceType, getOpcodeName } = require('./opcodes');
|
|
9
12
|
const { UserAgentPayload } = require('./userAgent');
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Загружает конфиг: { token, agent }
|
|
16
|
+
*/
|
|
17
|
+
function loadSessionConfig(configPath) {
|
|
18
|
+
let resolved;
|
|
19
|
+
if (path.isAbsolute(configPath)) {
|
|
20
|
+
resolved = configPath;
|
|
21
|
+
} else if (!/[\\/]/.test(configPath) && !configPath.endsWith('.json')) {
|
|
22
|
+
resolved = path.join(process.cwd(), 'config', `${configPath}.json`);
|
|
23
|
+
} else {
|
|
24
|
+
resolved = path.join(process.cwd(), configPath);
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(resolved)) {
|
|
27
|
+
throw new Error(`Конфиг не найден: ${resolved}`);
|
|
28
|
+
}
|
|
29
|
+
const data = fs.readFileSync(resolved, 'utf8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Понятная ошибка при отказе сервера отдать QR (часто qr_login.disabled для неофициального WEB-handshake).
|
|
35
|
+
*/
|
|
36
|
+
function throwIfGetQRRejected(payload) {
|
|
37
|
+
if (!payload || !payload.error) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const err = payload.error;
|
|
41
|
+
const text =
|
|
42
|
+
typeof err === 'string'
|
|
43
|
+
? err
|
|
44
|
+
: err && typeof err.message === 'string'
|
|
45
|
+
? err.message
|
|
46
|
+
: JSON.stringify(err);
|
|
47
|
+
if (String(text).includes('qr_login.disabled')) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Сервер Max отказал в выдаче QR (qr_login.disabled). Частые причины: устаревший appVersion в User-Agent (нужно ≥ 25.12.13), ' +
|
|
50
|
+
'или отключение QR для данного клиента на стороне VK. Проверьте https://web.max.ru в браузере. ' +
|
|
51
|
+
'Второй телефон к аккаунту можно добавить и обычным входом по номеру в приложении Max.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`QR request error: ${text}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
11
57
|
/**
|
|
12
58
|
* Основной клиент для работы с API Max
|
|
13
59
|
*/
|
|
@@ -18,20 +64,63 @@ class WebMaxClient extends EventEmitter {
|
|
|
18
64
|
this.phone = options.phone || null;
|
|
19
65
|
this.sessionName = options.name || options.session || 'default';
|
|
20
66
|
this.apiUrl = options.apiUrl || 'wss://ws-api.oneme.ru/websocket';
|
|
67
|
+
|
|
68
|
+
// Загрузка из config — token, ua (agent), device_type
|
|
69
|
+
let token = options.token || null;
|
|
70
|
+
let agent = options.ua || options.agent || options.headerUserAgent || null;
|
|
71
|
+
let configObj = {};
|
|
72
|
+
const configPath = options.configPath || options.config;
|
|
73
|
+
if (configPath) {
|
|
74
|
+
configObj = loadSessionConfig(configPath);
|
|
75
|
+
token = token || configObj.token || null;
|
|
76
|
+
agent = agent || configObj.agent || configObj.ua || configObj.headerUserAgent || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this._providedToken = token;
|
|
80
|
+
this._saveTokenToSession = options.saveToken !== false;
|
|
21
81
|
this.origin = 'https://web.max.ru';
|
|
22
82
|
this.session = new SessionManager(this.sessionName);
|
|
23
83
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
84
|
+
const deviceTypeMap = { 1: 'WEB', 2: 'IOS', 3: 'ANDROID' };
|
|
85
|
+
const rawDeviceType = options.deviceType ?? configObj.device_type ?? configObj.deviceType ?? this.session.get('deviceType');
|
|
86
|
+
const deviceType = deviceTypeMap[rawDeviceType] || rawDeviceType || 'WEB';
|
|
87
|
+
const uaString = agent || configObj.headerUserAgent || configObj.ua || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
|
|
88
|
+
const webDefaults = {
|
|
89
|
+
deviceType: deviceType,
|
|
90
|
+
locale: options.locale || configObj.locale || 'ru',
|
|
91
|
+
deviceLocale: options.deviceLocale || configObj.deviceLocale || configObj.locale || 'ru',
|
|
92
|
+
osVersion:
|
|
93
|
+
options.osVersion ||
|
|
94
|
+
configObj.osVersion ||
|
|
95
|
+
(deviceType === 'IOS' ? '18.6.2' : deviceType === 'ANDROID' ? '14' : 'Windows 11'),
|
|
96
|
+
deviceName:
|
|
97
|
+
options.deviceName ||
|
|
98
|
+
configObj.deviceName ||
|
|
99
|
+
(deviceType === 'IOS' ? 'Safari' : deviceType === 'ANDROID' ? 'Chrome' : 'Chrome'),
|
|
100
|
+
headerUserAgent: options.headerUserAgent || options.ua || uaString,
|
|
101
|
+
// Ниже 25.12.13 сервер может отвечать qr_login.disabled на GET_QR (см. PyMax _login).
|
|
102
|
+
appVersion: options.appVersion || configObj.appVersion || '25.12.14',
|
|
103
|
+
screen:
|
|
104
|
+
options.screen ||
|
|
105
|
+
configObj.screen ||
|
|
106
|
+
(deviceType === 'IOS' ? '390x844 3.0x' : deviceType === 'ANDROID' ? '360x780 3.0x' : '1080x1920 1.0x'),
|
|
107
|
+
timezone: options.timezone || configObj.timezone || 'Europe/Moscow',
|
|
108
|
+
buildNumber: options.buildNumber ?? configObj.buildNumber,
|
|
109
|
+
clientSessionId: options.clientSessionId ?? configObj.clientSessionId ?? this.session.get('clientSessionId'),
|
|
110
|
+
release: options.release ?? configObj.release
|
|
111
|
+
};
|
|
112
|
+
this._handshakeUserAgent = new UserAgentPayload(webDefaults);
|
|
113
|
+
this.userAgent = this._handshakeUserAgent;
|
|
28
114
|
|
|
29
|
-
// Device ID
|
|
30
115
|
this.deviceId = options.deviceId || this.session.get('deviceId') || uuidv4();
|
|
31
116
|
if (!this.session.get('deviceId')) {
|
|
32
117
|
this.session.set('deviceId', this.deviceId);
|
|
33
118
|
}
|
|
34
119
|
|
|
120
|
+
// Определяем тип транспорта: Socket для IOS/ANDROID, WebSocket для WEB
|
|
121
|
+
this._useSocketTransport = (deviceType === 'IOS' || deviceType === 'ANDROID');
|
|
122
|
+
this._socketTransport = null;
|
|
123
|
+
|
|
35
124
|
this.ws = null;
|
|
36
125
|
this.me = null;
|
|
37
126
|
this.isConnected = false;
|
|
@@ -55,6 +144,7 @@ class WebMaxClient extends EventEmitter {
|
|
|
55
144
|
|
|
56
145
|
this.messageQueue = [];
|
|
57
146
|
this.pendingRequests = new Map();
|
|
147
|
+
this.debug = options.debug || process.env.DEBUG === '1';
|
|
58
148
|
}
|
|
59
149
|
|
|
60
150
|
/**
|
|
@@ -135,22 +225,35 @@ class WebMaxClient extends EventEmitter {
|
|
|
135
225
|
try {
|
|
136
226
|
console.log('🚀 Запуск WebMax клиента...');
|
|
137
227
|
|
|
138
|
-
// Подключаемся к WebSocket
|
|
228
|
+
// Подключаемся к WebSocket или Socket
|
|
139
229
|
await this.connect();
|
|
140
230
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
231
|
+
// Приоритет: 1) переданный токен, 2) сохранённая сессия, 3) QR-авторизация
|
|
232
|
+
const tokenToUse = this._providedToken || this.session.get('token');
|
|
143
233
|
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
234
|
+
if (tokenToUse) {
|
|
235
|
+
if (this._providedToken) {
|
|
236
|
+
console.log('✅ Вход по токену (token auth)');
|
|
237
|
+
if (this._saveTokenToSession) {
|
|
238
|
+
this.session.set('token', this._providedToken);
|
|
239
|
+
this.session.set('deviceId', this.deviceId);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
console.log('✅ Найдена сохраненная сессия');
|
|
243
|
+
}
|
|
244
|
+
this._token = tokenToUse;
|
|
147
245
|
|
|
148
246
|
try {
|
|
149
247
|
await this.sync();
|
|
150
248
|
this.isAuthorized = true;
|
|
151
249
|
} catch (error) {
|
|
152
|
-
|
|
250
|
+
const wasTokenAuth = !!this._providedToken;
|
|
153
251
|
this.session.clear();
|
|
252
|
+
this._providedToken = null;
|
|
253
|
+
if (wasTokenAuth) {
|
|
254
|
+
throw new Error(`Токен недействителен или сессия истекла. Обновите токен в config. (${error.message})`);
|
|
255
|
+
}
|
|
256
|
+
console.log('⚠️ Сессия истекла, требуется повторная авторизация');
|
|
154
257
|
await this.authorize();
|
|
155
258
|
}
|
|
156
259
|
} else {
|
|
@@ -178,11 +281,9 @@ class WebMaxClient extends EventEmitter {
|
|
|
178
281
|
console.log('Запрос QR-кода для авторизации...');
|
|
179
282
|
|
|
180
283
|
const response = await this.sendAndWait(Opcode.GET_QR, {});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
284
|
+
|
|
285
|
+
throwIfGetQRRejected(response.payload);
|
|
286
|
+
|
|
186
287
|
return response.payload;
|
|
187
288
|
}
|
|
188
289
|
|
|
@@ -246,6 +347,127 @@ class WebMaxClient extends EventEmitter {
|
|
|
246
347
|
}
|
|
247
348
|
}
|
|
248
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Вывести в консоль QR-код для привязки нового устройства (тот же поток, что и веб-вход).
|
|
352
|
+
* Требуется уже авторизованная сессия (после SMS/QR и sync).
|
|
353
|
+
* На телефоне: Профиль → Устройства / Безопасность → Подключить устройство (QR).
|
|
354
|
+
*
|
|
355
|
+
* @param {object} [options]
|
|
356
|
+
* @param {boolean} [options.waitForScan=true] — ждать, пока QR отсканируют
|
|
357
|
+
* @param {boolean} [options.small=true] — компактный QR в терминале
|
|
358
|
+
* @returns {Promise<{ qrLink: string, trackId: string, pollingInterval: number, expiresAt: number }>}
|
|
359
|
+
*/
|
|
360
|
+
async showLinkDeviceQR(options = {}) {
|
|
361
|
+
const { waitForScan = true, small = true } = options;
|
|
362
|
+
|
|
363
|
+
if (!this.isConnected) {
|
|
364
|
+
throw new Error('Нет соединения: сначала await client.connect()');
|
|
365
|
+
}
|
|
366
|
+
if (!this.isAuthorized) {
|
|
367
|
+
throw new Error('Нужна авторизация: войдите в аккаунт и выполните sync, затем вызывайте showLinkDeviceQR');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// После LOGIN по TCP сервер не принимает GET_QR («Недопустимое состояние сессии») — тот же QR, что в веб-клиенте, только до авторизации по WebSocket.
|
|
371
|
+
if (this._useSocketTransport) {
|
|
372
|
+
return await this._showLinkDeviceQRViaEphemeralWeb(options);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log('Запрос QR-кода для привязки устройства...');
|
|
376
|
+
const response = await this.sendAndWait(Opcode.GET_QR, {});
|
|
377
|
+
|
|
378
|
+
throwIfGetQRRejected(response.payload);
|
|
379
|
+
|
|
380
|
+
const qrData = response.payload;
|
|
381
|
+
if (!qrData.qrLink || !qrData.trackId || !qrData.pollingInterval || !qrData.expiresAt) {
|
|
382
|
+
throw new Error('Неполные данные QR-кода от сервера');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await this._printLinkDeviceQRConsole(qrData.qrLink, small);
|
|
386
|
+
console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
|
|
387
|
+
console.log('='.repeat(70) + '\n');
|
|
388
|
+
|
|
389
|
+
if (waitForScan) {
|
|
390
|
+
await this.pollQRStatus(qrData.trackId, qrData.pollingInterval, qrData.expiresAt);
|
|
391
|
+
console.log('\n✅ Устройство подключено. Проверьте вход на телефоне.');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
qrLink: qrData.qrLink,
|
|
396
|
+
trackId: qrData.trackId,
|
|
397
|
+
pollingInterval: qrData.pollingInterval,
|
|
398
|
+
expiresAt: qrData.expiresAt
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
_printLinkDeviceQRConsole(qrLink, small = true) {
|
|
403
|
+
console.log('\n' + '='.repeat(70));
|
|
404
|
+
console.log('📱 ПРИВЯЗКА НОВОГО УСТРОЙСТВА');
|
|
405
|
+
console.log('='.repeat(70));
|
|
406
|
+
console.log('\nНа телефоне откройте Max — как при добавлении устройства в приложении:');
|
|
407
|
+
console.log('➡️ Профиль → Устройства / Безопасность → Подключить устройство (вход по QR)');
|
|
408
|
+
console.log('📸 Наведите камеру на QR ниже — это тот же поток, что у веб-клиента:\n');
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
qrcode.generate(qrLink, { small }, (qrCode) => {
|
|
411
|
+
console.log(qrCode);
|
|
412
|
+
resolve();
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* QR для привязки устройства при основой сессии на TCP (IOS/ANDROID): кратковременный WEB-клиент без LOGIN.
|
|
419
|
+
*/
|
|
420
|
+
async _showLinkDeviceQRViaEphemeralWeb(options = {}) {
|
|
421
|
+
const { waitForScan = true, small = true } = options;
|
|
422
|
+
const ephemeralName = `_link_qr_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
423
|
+
const webQr = new this.constructor({
|
|
424
|
+
name: ephemeralName,
|
|
425
|
+
deviceType: 'WEB',
|
|
426
|
+
debug: this.debug,
|
|
427
|
+
apiUrl: this.apiUrl,
|
|
428
|
+
origin: this.origin,
|
|
429
|
+
maxReconnectAttempts: 0
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
console.log(
|
|
434
|
+
'Отдельное WebSocket-подключение (как у web.max.ru): на уже залогиненном TCP запрос QR другим способом недоступен.'
|
|
435
|
+
);
|
|
436
|
+
await webQr.connect();
|
|
437
|
+
|
|
438
|
+
const response = await webQr.sendAndWait(Opcode.GET_QR, {});
|
|
439
|
+
throwIfGetQRRejected(response.payload);
|
|
440
|
+
|
|
441
|
+
const qrData = response.payload;
|
|
442
|
+
if (!qrData.qrLink || !qrData.trackId || !qrData.pollingInterval || !qrData.expiresAt) {
|
|
443
|
+
throw new Error('Неполные данные QR-кода от сервера');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await this._printLinkDeviceQRConsole(qrData.qrLink, small);
|
|
447
|
+
|
|
448
|
+
console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
|
|
449
|
+
console.log('='.repeat(70) + '\n');
|
|
450
|
+
|
|
451
|
+
if (waitForScan) {
|
|
452
|
+
await webQr.pollQRStatus(qrData.trackId, qrData.pollingInterval, qrData.expiresAt);
|
|
453
|
+
await webQr.loginByQR(qrData.trackId);
|
|
454
|
+
console.log('\n✅ Устройство подключено. Проверьте телефон.');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
qrLink: qrData.qrLink,
|
|
459
|
+
trackId: qrData.trackId,
|
|
460
|
+
pollingInterval: qrData.pollingInterval,
|
|
461
|
+
expiresAt: qrData.expiresAt
|
|
462
|
+
};
|
|
463
|
+
} finally {
|
|
464
|
+
try {
|
|
465
|
+
await webQr.stop();
|
|
466
|
+
webQr.session.destroy();
|
|
467
|
+
} catch (_) {}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
249
471
|
/**
|
|
250
472
|
* Авторизация через QR-код
|
|
251
473
|
*/
|
|
@@ -262,13 +484,15 @@ class WebMaxClient extends EventEmitter {
|
|
|
262
484
|
console.log('\n' + '='.repeat(70));
|
|
263
485
|
console.log('🔐 АВТОРИЗАЦИЯ ЧЕРЕЗ QR-КОД');
|
|
264
486
|
console.log('='.repeat(70));
|
|
265
|
-
console.log('\n📱
|
|
266
|
-
console.log('➡️ Настройки → Устройства → Подключить устройство');
|
|
487
|
+
console.log('\n📱 На телефоне: Профиль → Устройства / Безопасность → Подключить устройство');
|
|
267
488
|
console.log('📸 Отсканируйте QR-код ниже:\n');
|
|
268
489
|
|
|
269
|
-
// Отображаем QR-код в консоли
|
|
270
|
-
|
|
271
|
-
|
|
490
|
+
// Отображаем QR-код в консоли (ждём вывод, затем опрос статуса)
|
|
491
|
+
await new Promise((resolve) => {
|
|
492
|
+
qrcode.generate(qrData.qrLink, { small: true }, (qrCode) => {
|
|
493
|
+
console.log(qrCode);
|
|
494
|
+
resolve();
|
|
495
|
+
});
|
|
272
496
|
});
|
|
273
497
|
|
|
274
498
|
console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
|
|
@@ -288,12 +512,25 @@ class WebMaxClient extends EventEmitter {
|
|
|
288
512
|
throw new Error('Токен не получен из ответа');
|
|
289
513
|
}
|
|
290
514
|
|
|
515
|
+
// Сохраняем токен и все данные сессии для TCP подключения
|
|
291
516
|
this.session.set('token', token);
|
|
292
517
|
this.session.set('deviceId', this.deviceId);
|
|
518
|
+
this.session.set('clientSessionId', this.userAgent.clientSessionId);
|
|
519
|
+
this.session.set('deviceType', 'IOS'); // Переключаемся на IOS для TCP при следующем запуске
|
|
520
|
+
this.session.set('headerUserAgent', this.userAgent.headerUserAgent);
|
|
521
|
+
this.session.set('appVersion', this.userAgent.appVersion);
|
|
522
|
+
this.session.set('osVersion', this.userAgent.osVersion);
|
|
523
|
+
this.session.set('deviceName', this.userAgent.deviceName);
|
|
524
|
+
this.session.set('screen', this.userAgent.screen);
|
|
525
|
+
this.session.set('timezone', this.userAgent.timezone);
|
|
526
|
+
this.session.set('locale', this.userAgent.locale);
|
|
527
|
+
this.session.set('buildNumber', this.userAgent.buildNumber);
|
|
528
|
+
|
|
293
529
|
this.isAuthorized = true;
|
|
294
530
|
this._token = token;
|
|
295
531
|
|
|
296
532
|
console.log('✅ Авторизация через QR-код успешна!');
|
|
533
|
+
console.log('💡 При следующем запуске будет использоваться TCP Socket транспорт');
|
|
297
534
|
|
|
298
535
|
// Выполняем sync
|
|
299
536
|
await this.sync();
|
|
@@ -305,11 +542,105 @@ class WebMaxClient extends EventEmitter {
|
|
|
305
542
|
}
|
|
306
543
|
|
|
307
544
|
/**
|
|
308
|
-
* Авторизация
|
|
545
|
+
* Авторизация по номеру телефона через SMS (для IOS/ANDROID)
|
|
546
|
+
*/
|
|
547
|
+
async authorizeBySMS(phone) {
|
|
548
|
+
if (!this._useSocketTransport) {
|
|
549
|
+
throw new Error('SMS авторизация доступна только для IOS/ANDROID (используйте deviceType: "IOS" или "ANDROID")');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
console.log('📱 Авторизация по номеру телефона...');
|
|
554
|
+
|
|
555
|
+
// Нормализация номера телефона
|
|
556
|
+
let cleanPhone = phone.replace(/\D/g, '');
|
|
557
|
+
if (cleanPhone.startsWith('8') && cleanPhone.length === 11) {
|
|
558
|
+
cleanPhone = '7' + cleanPhone.slice(1);
|
|
559
|
+
} else if (cleanPhone.startsWith('9') && cleanPhone.length === 10) {
|
|
560
|
+
cleanPhone = '7' + cleanPhone;
|
|
561
|
+
}
|
|
562
|
+
const normalizedPhone = '+' + cleanPhone;
|
|
563
|
+
|
|
564
|
+
console.log(`📤 Запрос кода на номер: ${normalizedPhone}`);
|
|
565
|
+
|
|
566
|
+
if (!this._socketTransport) {
|
|
567
|
+
throw new Error('Socket транспорт не инициализирован');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Запрос кода
|
|
571
|
+
const tempToken = await this._socketTransport.requestCode(normalizedPhone);
|
|
572
|
+
|
|
573
|
+
if (!tempToken) {
|
|
574
|
+
throw new Error('Не получен временный токен');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
console.log('✅ Код отправлен! Ожидаем ввода кода...');
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
tempToken,
|
|
581
|
+
phone: normalizedPhone,
|
|
582
|
+
sendCode: async (code) => {
|
|
583
|
+
console.log('🔐 Проверка кода...');
|
|
584
|
+
|
|
585
|
+
const authResponse = await this._socketTransport.sendCode(tempToken, code);
|
|
586
|
+
|
|
587
|
+
if (authResponse?.passwordChallenge) {
|
|
588
|
+
throw new Error('2FA не поддерживается');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const token = authResponse?.tokenAttrs?.LOGIN?.token;
|
|
592
|
+
|
|
593
|
+
if (!token) {
|
|
594
|
+
throw new Error('Токен не получен из ответа');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Сохраняем сессию
|
|
598
|
+
this.session.set('token', token);
|
|
599
|
+
this.session.set('deviceId', this.deviceId);
|
|
600
|
+
this.session.set('clientSessionId', this.userAgent.clientSessionId);
|
|
601
|
+
this.session.set('deviceType', this.userAgent.deviceType);
|
|
602
|
+
this.session.set('headerUserAgent', this.userAgent.headerUserAgent);
|
|
603
|
+
this.session.set('appVersion', this.userAgent.appVersion);
|
|
604
|
+
this.session.set('osVersion', this.userAgent.osVersion);
|
|
605
|
+
this.session.set('deviceName', this.userAgent.deviceName);
|
|
606
|
+
this.session.set('screen', this.userAgent.screen);
|
|
607
|
+
this.session.set('timezone', this.userAgent.timezone);
|
|
608
|
+
this.session.set('locale', this.userAgent.locale);
|
|
609
|
+
this.session.set('buildNumber', this.userAgent.buildNumber);
|
|
610
|
+
|
|
611
|
+
this.isAuthorized = true;
|
|
612
|
+
this._token = token;
|
|
613
|
+
|
|
614
|
+
console.log('✅ Авторизация по SMS успешна!');
|
|
615
|
+
|
|
616
|
+
// Выполняем sync
|
|
617
|
+
await this.sync();
|
|
618
|
+
|
|
619
|
+
return token;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error('Ошибка SMS авторизации:', error);
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Авторизация пользователя (QR-код для WEB, SMS для IOS/ANDROID)
|
|
309
631
|
*/
|
|
310
|
-
async authorize() {
|
|
311
|
-
|
|
312
|
-
|
|
632
|
+
async authorize(phone = null) {
|
|
633
|
+
if (this._useSocketTransport && phone) {
|
|
634
|
+
// SMS авторизация для IOS/ANDROID
|
|
635
|
+
console.log('🔐 Авторизация через SMS');
|
|
636
|
+
return await this.authorizeBySMS(phone);
|
|
637
|
+
} else if (this._useSocketTransport && !phone) {
|
|
638
|
+
throw new Error('Для IOS/ANDROID требуется номер телефона. Используйте: authorize("+79001234567")');
|
|
639
|
+
} else {
|
|
640
|
+
// QR авторизация для WEB
|
|
641
|
+
console.log('🔐 Авторизация через QR-код');
|
|
642
|
+
await this.authorizeByQR();
|
|
643
|
+
}
|
|
313
644
|
}
|
|
314
645
|
|
|
315
646
|
|
|
@@ -332,14 +663,16 @@ class WebMaxClient extends EventEmitter {
|
|
|
332
663
|
contactsSync: 0,
|
|
333
664
|
presenceSync: 0,
|
|
334
665
|
draftsSync: 0,
|
|
335
|
-
chatsCount: 40
|
|
336
|
-
userAgent: this.userAgent.toJSON()
|
|
666
|
+
chatsCount: 40
|
|
337
667
|
};
|
|
668
|
+
payload.userAgent = this.userAgent.toJSON();
|
|
338
669
|
|
|
339
670
|
const response = await this.sendAndWait(Opcode.LOGIN, payload);
|
|
340
671
|
|
|
341
672
|
if (response.payload && response.payload.error) {
|
|
342
|
-
|
|
673
|
+
const err = response.payload.error;
|
|
674
|
+
const msg = typeof err === 'string' ? err : (response.payload.localizedMessage || JSON.stringify(err));
|
|
675
|
+
throw new Error(msg);
|
|
343
676
|
}
|
|
344
677
|
|
|
345
678
|
// Сохраняем информацию о пользователе
|
|
@@ -421,17 +754,58 @@ class WebMaxClient extends EventEmitter {
|
|
|
421
754
|
|
|
422
755
|
|
|
423
756
|
/**
|
|
424
|
-
* Установка WebSocket
|
|
757
|
+
* Установка соединения (WebSocket или Socket)
|
|
425
758
|
*/
|
|
426
759
|
async connect() {
|
|
760
|
+
if (this._useSocketTransport) {
|
|
761
|
+
return this._connectSocket();
|
|
762
|
+
} else {
|
|
763
|
+
return this._connectWebSocket();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Подключение через TCP Socket (для IOS/ANDROID)
|
|
769
|
+
*/
|
|
770
|
+
async _connectSocket() {
|
|
771
|
+
if (this._socketTransport && this._socketTransport.socket && !this._socketTransport.socket.destroyed) {
|
|
772
|
+
this.isConnected = true;
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this._socketTransport = new MaxSocketTransport({
|
|
777
|
+
deviceId: this.deviceId,
|
|
778
|
+
deviceType: this.userAgent.deviceType,
|
|
779
|
+
ua: this.userAgent.headerUserAgent,
|
|
780
|
+
debug: this.debug
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
this._socketTransport.onNotification = (data) => {
|
|
784
|
+
this.handleSocketNotification(data);
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
await this._socketTransport.connect();
|
|
788
|
+
await this._socketTransport.handshake(this.userAgent);
|
|
789
|
+
|
|
790
|
+
this.isConnected = true;
|
|
791
|
+
this.reconnectAttempts = 0;
|
|
792
|
+
this.emit('connected');
|
|
793
|
+
|
|
794
|
+
console.log('TCP Socket соединение установлено');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Установка WebSocket соединения (для WEB)
|
|
799
|
+
*/
|
|
800
|
+
async _connectWebSocket() {
|
|
427
801
|
if (this.ws && this.isConnected) {
|
|
428
802
|
return;
|
|
429
803
|
}
|
|
430
804
|
|
|
431
805
|
return new Promise((resolve, reject) => {
|
|
432
806
|
const headers = {
|
|
433
|
-
'
|
|
434
|
-
'
|
|
807
|
+
'Origin': this.origin,
|
|
808
|
+
'User-Agent': this._handshakeUserAgent.headerUserAgent
|
|
435
809
|
};
|
|
436
810
|
|
|
437
811
|
this.ws = new WebSocket(this.apiUrl, {
|
|
@@ -445,7 +819,6 @@ class WebMaxClient extends EventEmitter {
|
|
|
445
819
|
this.emit('connected');
|
|
446
820
|
|
|
447
821
|
try {
|
|
448
|
-
// Выполняем handshake
|
|
449
822
|
await this.handshake();
|
|
450
823
|
resolve();
|
|
451
824
|
} catch (error) {
|
|
@@ -466,6 +839,12 @@ class WebMaxClient extends EventEmitter {
|
|
|
466
839
|
this.ws.on('close', () => {
|
|
467
840
|
console.log('WebSocket соединение закрыто');
|
|
468
841
|
this.isConnected = false;
|
|
842
|
+
const err = new Error('Соединение закрыто');
|
|
843
|
+
for (const [, pending] of this.pendingRequests) {
|
|
844
|
+
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
|
845
|
+
pending.reject(err);
|
|
846
|
+
}
|
|
847
|
+
this.pendingRequests.clear();
|
|
469
848
|
this.triggerHandlers(EventTypes.DISCONNECT);
|
|
470
849
|
this.handleReconnect();
|
|
471
850
|
});
|
|
@@ -480,7 +859,7 @@ class WebMaxClient extends EventEmitter {
|
|
|
480
859
|
|
|
481
860
|
const payload = {
|
|
482
861
|
deviceId: this.deviceId,
|
|
483
|
-
userAgent: this.
|
|
862
|
+
userAgent: this._handshakeUserAgent.toJSON()
|
|
484
863
|
};
|
|
485
864
|
|
|
486
865
|
const response = await this.sendAndWait(Opcode.SESSION_INIT, payload);
|
|
@@ -493,6 +872,41 @@ class WebMaxClient extends EventEmitter {
|
|
|
493
872
|
return response;
|
|
494
873
|
}
|
|
495
874
|
|
|
875
|
+
/**
|
|
876
|
+
* Обработка уведомлений от Socket транспорта
|
|
877
|
+
*/
|
|
878
|
+
async handleSocketNotification(data) {
|
|
879
|
+
try {
|
|
880
|
+
if (this.debug && data.opcode !== Opcode.PING) {
|
|
881
|
+
const payload = data.payload?.error ? ` error=${JSON.stringify(data.payload.error)}` : '';
|
|
882
|
+
console.log(`📥 ${getOpcodeName(data.opcode)} (seq=${data.seq})${payload}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
switch (data.opcode) {
|
|
886
|
+
case Opcode.NOTIF_MESSAGE:
|
|
887
|
+
await this.handleNewMessage(data.payload);
|
|
888
|
+
break;
|
|
889
|
+
|
|
890
|
+
case Opcode.NOTIF_MSG_DELETE:
|
|
891
|
+
await this.handleRemovedMessage(data.payload);
|
|
892
|
+
break;
|
|
893
|
+
|
|
894
|
+
case Opcode.NOTIF_CHAT:
|
|
895
|
+
await this.handleChatAction(data.payload);
|
|
896
|
+
break;
|
|
897
|
+
|
|
898
|
+
case Opcode.PING:
|
|
899
|
+
break;
|
|
900
|
+
|
|
901
|
+
default:
|
|
902
|
+
this.emit('raw_message', data);
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.error('Ошибка при обработке Socket уведомления:', error);
|
|
906
|
+
await this.triggerHandlers(EventTypes.ERROR, error);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
496
910
|
/**
|
|
497
911
|
* Обработка переподключения
|
|
498
912
|
*/
|
|
@@ -510,16 +924,16 @@ class WebMaxClient extends EventEmitter {
|
|
|
510
924
|
}
|
|
511
925
|
|
|
512
926
|
/**
|
|
513
|
-
* Обработка входящих сообщений
|
|
927
|
+
* Обработка входящих сообщений (WebSocket)
|
|
514
928
|
*/
|
|
515
929
|
async handleMessage(data) {
|
|
516
930
|
try {
|
|
517
931
|
const message = JSON.parse(data.toString());
|
|
518
932
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
933
|
+
if (this.debug && message.opcode !== Opcode.PING) {
|
|
934
|
+
const payload = message.payload?.error ? ` error=${JSON.stringify(message.payload.error)}` : '';
|
|
935
|
+
console.log(`📥 ${getOpcodeName(message.opcode)} (seq=${message.seq})${payload}`);
|
|
936
|
+
}
|
|
523
937
|
|
|
524
938
|
// Обработка ответов на запросы по seq
|
|
525
939
|
if (message.seq && this.pendingRequests.has(message.seq)) {
|
|
@@ -549,7 +963,6 @@ class WebMaxClient extends EventEmitter {
|
|
|
549
963
|
break;
|
|
550
964
|
|
|
551
965
|
case Opcode.PING:
|
|
552
|
-
// Отвечаем на ping (без логирования)
|
|
553
966
|
await this.sendPong();
|
|
554
967
|
break;
|
|
555
968
|
|
|
@@ -629,21 +1042,25 @@ class WebMaxClient extends EventEmitter {
|
|
|
629
1042
|
}
|
|
630
1043
|
|
|
631
1044
|
/**
|
|
632
|
-
* Отправка запроса
|
|
1045
|
+
* Отправка запроса и ожидание ответа
|
|
633
1046
|
*/
|
|
634
|
-
sendAndWait(opcode, payload, cmd = 0, timeout = 20000) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1047
|
+
async sendAndWait(opcode, payload, cmd = 0, timeout = 20000) {
|
|
1048
|
+
if (!this.isConnected) {
|
|
1049
|
+
throw new Error('Соединение не установлено');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Используем Socket транспорт для IOS/ANDROID
|
|
1053
|
+
if (this._useSocketTransport && this._socketTransport) {
|
|
1054
|
+
return await this._socketTransport.sendAndWait(opcode, payload, cmd, timeout);
|
|
1055
|
+
}
|
|
640
1056
|
|
|
1057
|
+
// WebSocket транспорт для WEB
|
|
1058
|
+
return new Promise((resolve, reject) => {
|
|
641
1059
|
const message = this.makeMessage(opcode, payload, cmd);
|
|
642
1060
|
const seq = message.seq;
|
|
643
1061
|
|
|
644
1062
|
this.pendingRequests.set(seq, { resolve, reject });
|
|
645
1063
|
|
|
646
|
-
// Таймаут для запроса
|
|
647
1064
|
const timeoutId = setTimeout(() => {
|
|
648
1065
|
if (this.pendingRequests.has(seq)) {
|
|
649
1066
|
this.pendingRequests.delete(seq);
|
|
@@ -651,19 +1068,14 @@ class WebMaxClient extends EventEmitter {
|
|
|
651
1068
|
}
|
|
652
1069
|
}, timeout);
|
|
653
1070
|
|
|
654
|
-
// Сохраняем timeoutId чтобы можно было отменить
|
|
655
1071
|
this.pendingRequests.get(seq).timeoutId = timeoutId;
|
|
656
1072
|
|
|
657
|
-
// Отладочное логирование (раскомментируйте при необходимости)
|
|
658
|
-
// if (opcode !== Opcode.PING) {
|
|
659
|
-
// console.log(`📤 Отправка: ${getOpcodeName(opcode)} (seq=${seq})`);
|
|
660
|
-
// }
|
|
661
1073
|
this.ws.send(JSON.stringify(message));
|
|
662
1074
|
});
|
|
663
1075
|
}
|
|
664
1076
|
|
|
665
1077
|
/**
|
|
666
|
-
* Отправка сообщения
|
|
1078
|
+
* Отправка сообщения (с уведомлением)
|
|
667
1079
|
*/
|
|
668
1080
|
async sendMessage(options) {
|
|
669
1081
|
if (typeof options === 'string') {
|
|
@@ -672,6 +1084,37 @@ class WebMaxClient extends EventEmitter {
|
|
|
672
1084
|
|
|
673
1085
|
const { chatId, text, cid, replyTo, attachments } = options;
|
|
674
1086
|
|
|
1087
|
+
const payload = {
|
|
1088
|
+
chatId: chatId,
|
|
1089
|
+
message: {
|
|
1090
|
+
text: text || '',
|
|
1091
|
+
cid: cid || Date.now(),
|
|
1092
|
+
elements: [],
|
|
1093
|
+
attaches: attachments || [],
|
|
1094
|
+
link: replyTo ? { type: 'REPLY', messageId: replyTo } : null
|
|
1095
|
+
},
|
|
1096
|
+
notify: true
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const response = await this.sendAndWait(Opcode.MSG_SEND, payload);
|
|
1100
|
+
|
|
1101
|
+
if (response.payload && response.payload.message) {
|
|
1102
|
+
return new Message(response.payload.message, this);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return response.payload;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Отправка сообщения в канал (без уведомления)
|
|
1110
|
+
*/
|
|
1111
|
+
async sendMessageChannel(options) {
|
|
1112
|
+
if (typeof options === 'string') {
|
|
1113
|
+
throw new Error('sendMessageChannel требует объект с параметрами: { chatId, text, cid }');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const { chatId, text, cid, replyTo, attachments } = options;
|
|
1117
|
+
|
|
675
1118
|
const payload = {
|
|
676
1119
|
chatId: chatId,
|
|
677
1120
|
message: {
|
|
@@ -769,6 +1212,10 @@ class WebMaxClient extends EventEmitter {
|
|
|
769
1212
|
* Получение списка чатов
|
|
770
1213
|
*/
|
|
771
1214
|
async getChats(marker = 0) {
|
|
1215
|
+
if (this._useSocketTransport && this._socketTransport) {
|
|
1216
|
+
return await this._socketTransport.getChats(marker);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
772
1219
|
const payload = {
|
|
773
1220
|
marker: marker
|
|
774
1221
|
};
|
|
@@ -782,6 +1229,11 @@ class WebMaxClient extends EventEmitter {
|
|
|
782
1229
|
* Получение истории сообщений
|
|
783
1230
|
*/
|
|
784
1231
|
async getHistory(chatId, from = Date.now(), backward = 200, forward = 0) {
|
|
1232
|
+
if (this._useSocketTransport && this._socketTransport) {
|
|
1233
|
+
const messages = await this._socketTransport.getHistory(chatId, from, backward, forward);
|
|
1234
|
+
return messages.map(msg => new Message(msg, this));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
785
1237
|
const payload = {
|
|
786
1238
|
chatId: chatId,
|
|
787
1239
|
from: from,
|
|
@@ -819,6 +1271,10 @@ class WebMaxClient extends EventEmitter {
|
|
|
819
1271
|
* Остановка клиента
|
|
820
1272
|
*/
|
|
821
1273
|
async stop() {
|
|
1274
|
+
if (this._socketTransport) {
|
|
1275
|
+
await this._socketTransport.close();
|
|
1276
|
+
this._socketTransport = null;
|
|
1277
|
+
}
|
|
822
1278
|
if (this.ws) {
|
|
823
1279
|
this.ws.close();
|
|
824
1280
|
this.ws = null;
|