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.
- package/README.md +64 -6
- package/lib/client.js +176 -21
- 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
|
-
📱
|
|
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.
|
|
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
|
-
|
|
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:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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📱
|
|
335
|
-
console.log('➡️ Настройки → Устройства → Подключить устройство');
|
|
487
|
+
console.log('\n📱 На телефоне: Профиль → Устройства / Безопасность → Подключить устройство');
|
|
336
488
|
console.log('📸 Отсканируйте QR-код ниже:\n');
|
|
337
489
|
|
|
338
|
-
// Отображаем QR-код в консоли
|
|
339
|
-
|
|
340
|
-
|
|
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);
|