webmaxsocket 1.1.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.
Files changed (3) hide show
  1. package/README.md +64 -6
  2. package/lib/client.js +176 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  ## ✨ Особенности / Features
8
8
 
9
9
  - ✅ **QR-код авторизация** / QR code authentication
10
+ - ✅ **QR для привязки устройства** (`showLinkDeviceQR`) после входа по SMS/TCP — тот же сценарий, что «Профиль → Устройства → Подключить устройство» в приложении
10
11
  - ✅ **Token авторизация** / Token authentication
11
12
  - ✅ **Два транспорта:** WebSocket (WEB) и TCP Socket (IOS/ANDROID)
12
13
  - ✅ **Автоматическое сохранение сессий** / Automatic session storage
@@ -83,8 +84,7 @@ main().catch(console.error);
83
84
  🔐 АВТОРИЗАЦИЯ ЧЕРЕЗ QR-КОД
84
85
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85
86
 
86
- 📱 Откройте приложение Max на телефоне
87
- ➡️ Настройки → Устройства → Подключить устройство
87
+ 📱 На телефоне: Профиль Устройства / Безопасность → Подключить устройство
88
88
  📸 Отсканируйте QR-код
89
89
 
90
90
  █████████████████████████████
@@ -126,6 +126,29 @@ node example-sms.js
126
126
  node example-sms.js +79001234567 # с номером в аргументе
127
127
  ```
128
128
 
129
+ #### QR после входа: привязка второго устройства (IOS/ANDROID)
130
+
131
+ Когда вы уже авторизованы по **TCP** (SMS и сохранённая сессия), запрос **`GET_QR` на том же соединении недоступен** (ответ сервера: недопустимое состояние сессии). Для сценария как в приложении — **показать QR, телефон сканирует** — используйте метод **`showLinkDeviceQR()`**: библиотека открывает **отдельное краткоживущее WebSocket-подключение** (как у [web.max.ru](https://web.max.ru)), запрашивает QR, печатает его в консоль и при необходимости ждёт сканирования.
132
+
133
+ Требования: активное соединение и **`isAuthorized`** (обычно после `await client.start()`).
134
+
135
+ ```javascript
136
+ await client.start();
137
+
138
+ // Показать QR и ждать, пока отсканируют в приложении Max на телефоне
139
+ await client.showLinkDeviceQR();
140
+
141
+ // Только показать QR и вернуть данные (без ожидания скана)
142
+ const data = await client.showLinkDeviceQR({ waitForScan: false });
143
+ // data: { qrLink, trackId, pollingInterval, expiresAt }
144
+ ```
145
+
146
+ Опции: `waitForScan` (по умолчанию `true`), `small` — компактный QR в терминале.
147
+
148
+ **Версия клиента:** для выдачи QR сервер ожидает актуальный **`appVersion`** в User-Agent (не ниже **25.12.13**). В конструкторе по умолчанию используется **25.12.14**; при необходимости передайте `appVersion: '25.21.3'` или новее.
149
+
150
+ Если сервер отвечает **`qr_login.disabled`**, проверьте версию приложения в опциях, откройте [web.max.ru](https://web.max.ru) в браузере или войдите на втором устройстве по номеру телефона.
151
+
129
152
  #### Способ 3: Token авторизация
130
153
 
131
154
  Если у вас уже есть токен (от другого сервиса/приложения):
@@ -172,12 +195,21 @@ const client = new WebMaxClient({
172
195
  name: 'session', // Имя сессии (для сохранения авторизации)
173
196
  token: 'An_Sx6H...', // Токен авторизации (опционально)
174
197
  configPath: 'myconfig', // Путь к config файлу (опционально)
175
- deviceType: 'WEB', // Тип устройства: 'WEB', 'IOS', 'ANDROID' (опционально)
198
+ deviceType: 'WEB', // Тип устройства: 'WEB', 'IOS', 'ANDROID', 'DESKTOP' (опционально)
176
199
  saveToken: true, // Сохранять токен в сессию (по умолчанию true)
177
200
  debug: false, // Отладочный режим (опционально)
178
201
  apiUrl: 'wss://...', // URL WebSocket API (опционально)
179
202
  maxReconnectAttempts: 5,// Максимальное количество попыток переподключения
180
- reconnectDelay: 3000 // Задержка между попытками переподключения (мс)
203
+ reconnectDelay: 3000, // Задержка между попытками переподключения (мс)
204
+ // User-Agent / клиент (важно для GET_QR, см. showLinkDeviceQR):
205
+ appVersion: '25.12.14', // Рекомендуется ≥ 25.12.13 для запроса QR
206
+ ua: 'Mozilla/5.0 ...', // или headerUserAgent
207
+ osVersion: 'Windows 11',
208
+ screen: '1920x1080 1.0x',
209
+ timezone: 'Europe/Moscow',
210
+ locale: 'ru',
211
+ buildNumber: 0x97cb, // опционально
212
+ clientSessionId: 1 // опционально
181
213
  });
182
214
  ```
183
215
 
@@ -206,6 +238,24 @@ const authSession = await client.authorizeBySMS('+79001234567');
206
238
  await authSession.sendCode('123456');
207
239
  ```
208
240
 
241
+ ##### `showLinkDeviceQR(options)`
242
+
243
+ Показать в консоли **QR-код для привязки устройства** (как в приложении Max: телефон сканирует QR). Нужна **уже выполненная авторизация** (`start()` или `connect` + `sync`).
244
+
245
+ - Для **WEB** запрос выполняется по текущему WebSocket.
246
+ - Для **IOS/ANDROID** после входа по TCP используется **второе** WebSocket-подключение без повторного `LOGIN` на той сессии (иначе `GET_QR` на том же TCP недоступен).
247
+
248
+ ```javascript
249
+ await client.showLinkDeviceQR();
250
+ await client.showLinkDeviceQR({ waitForScan: false, small: false });
251
+ ```
252
+
253
+ Возвращает `Promise<{ qrLink, trackId, pollingInterval, expiresAt }>`.
254
+
255
+ ##### `requestQR()`, `checkQRStatus(trackId)`, `loginByQR(trackId)`, `authorizeByQR()`
256
+
257
+ Низкоуровневые шаги QR-авторизации для **WEB** (первый вход без SMS). Обычно достаточно `start()` без токена или `authorizeByQR()`.
258
+
209
259
  ##### `sendMessage(options)`
210
260
 
211
261
  Отправляет сообщение в чат с уведомлением (notify: true).
@@ -532,6 +582,10 @@ node example-ios.js
532
582
  node example-ios.js --debug
533
583
  ```
534
584
 
585
+ ### Пример 5: QR для второго устройства после SMS
586
+
587
+ После успешного `start()` с сохранённой сессией IOS/Android вызовите `showLinkDeviceQR()` (см. раздел **«QR после входа»** выше).
588
+
535
589
  ## Структура проекта
536
590
 
537
591
  ```
@@ -609,11 +663,15 @@ DEBUG=1 node example.js
609
663
 
610
664
  1. **TCP Socket после QR-авторизации:** После первой успешной QR-авторизации клиент автоматически сохраняет `clientSessionId` и переключается на TCP Socket транспорт при следующем запуске для повышения стабильности.
611
665
 
612
- 2. **Разница между sendMessage и sendMessageChannel:**
666
+ 2. **QR для нового устройства после входа по SMS/TCP:** Используйте `showLinkDeviceQR()`. Это не отдельный опкод в протоколе, а тот же `GET_QR`, что и у веб-клиента; для уже залогиненного TCP-сокета запрос выполняется через **эфемерное WebSocket-подключение** (временный файл сессии `_link_qr_*` удаляется после завершения).
667
+
668
+ 3. **Версия `appVersion` и QR:** Слишком старая версия в User-Agent может привести к ответу `qr_login.disabled` на `GET_QR`. Задайте в конструкторе актуальную строку (по умолчанию **25.12.14**).
669
+
670
+ 4. **Разница между sendMessage и sendMessageChannel:**
613
671
  - `sendMessage()` - отправка с уведомлением (notify: true) для обычных чатов
614
672
  - `sendMessageChannel()` - отправка без уведомления (notify: false) для каналов
615
673
 
616
- 3. **Автоматический выбор транспорта:** Клиент автоматически определяет какой транспорт использовать на основе `deviceType` в сессии или config файле.
674
+ 5. **Автоматический выбор транспорта:** Клиент автоматически определяет какой транспорт использовать на основе `deviceType` в сессии или config файле.
617
675
 
618
676
  ## 🔗 Ссылки / Links
619
677
 
package/lib/client.js CHANGED
@@ -30,6 +30,30 @@ function loadSessionConfig(configPath) {
30
30
  return JSON.parse(data);
31
31
  }
32
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
+
33
57
  /**
34
58
  * Основной клиент для работы с API Max
35
59
  */
@@ -63,17 +87,27 @@ class WebMaxClient extends EventEmitter {
63
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';
64
88
  const webDefaults = {
65
89
  deviceType: deviceType,
66
- locale: configObj.locale || 'ru',
67
- deviceLocale: configObj.deviceLocale || configObj.locale || 'ru',
68
- osVersion: configObj.osVersion || (deviceType === 'IOS' ? '18.6.2' : deviceType === 'ANDROID' ? '14' : 'Linux'),
69
- deviceName: configObj.deviceName || (deviceType === 'IOS' ? 'Safari' : deviceType === 'ANDROID' ? 'Chrome' : 'Chrome'),
70
- headerUserAgent: uaString,
71
- appVersion: configObj.appVersion || '25.10.5',
72
- screen: configObj.screen || (deviceType === 'IOS' ? '390x844 3.0x' : deviceType === 'ANDROID' ? '360x780 3.0x' : '1080x1920 1.0x'),
73
- timezone: configObj.timezone || 'Europe/Moscow',
74
- buildNumber: configObj.buildNumber,
75
- clientSessionId: configObj.clientSessionId || this.session.get('clientSessionId'),
76
- release: configObj.release
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
77
111
  };
78
112
  this._handshakeUserAgent = new UserAgentPayload(webDefaults);
79
113
  this.userAgent = this._handshakeUserAgent;
@@ -247,11 +281,9 @@ class WebMaxClient extends EventEmitter {
247
281
  console.log('Запрос QR-кода для авторизации...');
248
282
 
249
283
  const response = await this.sendAndWait(Opcode.GET_QR, {});
250
-
251
- if (response.payload && response.payload.error) {
252
- throw new Error(`QR request error: ${JSON.stringify(response.payload.error)}`);
253
- }
254
-
284
+
285
+ throwIfGetQRRejected(response.payload);
286
+
255
287
  return response.payload;
256
288
  }
257
289
 
@@ -315,6 +347,127 @@ class WebMaxClient extends EventEmitter {
315
347
  }
316
348
  }
317
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
+
318
471
  /**
319
472
  * Авторизация через QR-код
320
473
  */
@@ -331,13 +484,15 @@ class WebMaxClient extends EventEmitter {
331
484
  console.log('\n' + '='.repeat(70));
332
485
  console.log('🔐 АВТОРИЗАЦИЯ ЧЕРЕЗ QR-КОД');
333
486
  console.log('='.repeat(70));
334
- console.log('\n📱 Откройте приложение Max на телефоне');
335
- console.log('➡️ Настройки → Устройства → Подключить устройство');
487
+ console.log('\n📱 На телефоне: Профиль Устройства / Безопасность → Подключить устройство');
336
488
  console.log('📸 Отсканируйте QR-код ниже:\n');
337
489
 
338
- // Отображаем QR-код в консоли
339
- qrcode.generate(qrData.qrLink, { small: true }, (qrCode) => {
340
- 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
+ });
341
496
  });
342
497
 
343
498
  console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webmaxsocket",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Node.js client for Max Messenger with QR code and token authentication",
5
5
  "main": "index.js",
6
6
  "scripts": {