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/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
- // UserAgent
25
- this.userAgent = options.userAgent || new UserAgentPayload({
26
- appVersion: options.appVersion || '25.12.14'
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 savedToken = this.session.get('token');
231
+ // Приоритет: 1) переданный токен, 2) сохранённая сессия, 3) QR-авторизация
232
+ const tokenToUse = this._providedToken || this.session.get('token');
143
233
 
144
- if (savedToken) {
145
- console.log('✅ Найдена сохраненная сессия');
146
- this._token = savedToken;
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
- console.log('⚠️ Сессия истекла, требуется повторная авторизация');
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
- if (response.payload && response.payload.error) {
183
- throw new Error(`QR request error: ${JSON.stringify(response.payload.error)}`);
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📱 Откройте приложение Max на телефоне');
266
- console.log('➡️ Настройки → Устройства → Подключить устройство');
487
+ console.log('\n📱 На телефоне: Профиль Устройства / Безопасность → Подключить устройство');
267
488
  console.log('📸 Отсканируйте QR-код ниже:\n');
268
489
 
269
- // Отображаем QR-код в консоли
270
- qrcode.generate(qrData.qrLink, { small: true }, (qrCode) => {
271
- console.log(qrCode);
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
- * Авторизация пользователя через QR-код
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
- console.log('🔐 Авторизация через QR-код');
312
- await this.authorizeByQR();
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
- throw new Error(`Sync error: ${JSON.stringify(response.payload.error)}`);
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
- 'User-Agent': this.userAgent.headerUserAgent,
434
- 'Origin': this.origin
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.userAgent.toJSON()
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
- // if (message.opcode !== Opcode.PING) {
521
- // console.log(`📥 Получено: ${getOpcodeName(message.opcode)} (seq=${message.seq})`);
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
- * Отправка запроса через WebSocket и ожидание ответа
1045
+ * Отправка запроса и ожидание ответа
633
1046
  */
634
- sendAndWait(opcode, payload, cmd = 0, timeout = 20000) {
635
- return new Promise((resolve, reject) => {
636
- if (!this.isConnected) {
637
- reject(new Error('WebSocket не подключен'));
638
- return;
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;