webmaxsocket 1.1.0 → 1.1.2

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 CHANGED
@@ -7,14 +7,17 @@
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
13
14
  - ✅ **Автовыбор транспорта** после QR-авторизации (переход на TCP)
14
15
  - ✅ **Отправка и получение сообщений** / Send and receive messages
16
+ - ✅ **Скачивание вложений по URL** (`baseUrl` из `attaches`) во временный файл — `downloadUrlToTempFile`, `message.downloadAttachment()`
15
17
  - ✅ **Редактирование и удаление сообщений** / Edit and delete messages
16
18
  - ✅ **Event-driven архитектура** / Event-driven architecture
17
19
  - ✅ **Обработка входящих уведомлений** / Handle incoming notifications
20
+ - ✅ **Встроенный лог входящих** (`logIncoming`, `WEBMAX_DEBUG`, `WEBMAX_SILENT`) — JSON в консоль без ручных обработчиков
18
21
  - ✅ **TypeScript-ready** структура / TypeScript-ready structure
19
22
 
20
23
  ## 📦 Установка / Installation
@@ -61,8 +64,7 @@ async function main() {
61
64
 
62
65
  // Автоответ / Auto-reply
63
66
  await message.reply({
64
- text: `Привет! Я получил: "${message.text}"`,
65
- cid: Date.now()
67
+ text: `Привет! Я получил: "${message.text}"`
66
68
  });
67
69
  });
68
70
 
@@ -83,8 +85,7 @@ main().catch(console.error);
83
85
  🔐 АВТОРИЗАЦИЯ ЧЕРЕЗ QR-КОД
84
86
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85
87
 
86
- 📱 Откройте приложение Max на телефоне
87
- ➡️ Настройки → Устройства → Подключить устройство
88
+ 📱 На телефоне: Профиль Устройства / Безопасность → Подключить устройство
88
89
  📸 Отсканируйте QR-код
89
90
 
90
91
  █████████████████████████████
@@ -126,6 +127,29 @@ node example-sms.js
126
127
  node example-sms.js +79001234567 # с номером в аргументе
127
128
  ```
128
129
 
130
+ #### QR после входа: привязка второго устройства (IOS/ANDROID)
131
+
132
+ Когда вы уже авторизованы по **TCP** (SMS и сохранённая сессия), запрос **`GET_QR` на том же соединении недоступен** (ответ сервера: недопустимое состояние сессии). Для сценария как в приложении — **показать QR, телефон сканирует** — используйте метод **`showLinkDeviceQR()`**: библиотека открывает **отдельное краткоживущее WebSocket-подключение** (как у [web.max.ru](https://web.max.ru)), запрашивает QR, печатает его в консоль и при необходимости ждёт сканирования.
133
+
134
+ Требования: активное соединение и **`isAuthorized`** (обычно после `await client.start()`).
135
+
136
+ ```javascript
137
+ await client.start();
138
+
139
+ // Показать QR и ждать, пока отсканируют в приложении Max на телефоне
140
+ await client.showLinkDeviceQR();
141
+
142
+ // Только показать QR и вернуть данные (без ожидания скана)
143
+ const data = await client.showLinkDeviceQR({ waitForScan: false });
144
+ // data: { qrLink, trackId, pollingInterval, expiresAt }
145
+ ```
146
+
147
+ Опции: `waitForScan` (по умолчанию `true`), `small` — компактный QR в терминале.
148
+
149
+ **Версия клиента:** для выдачи QR сервер ожидает актуальный **`appVersion`** в User-Agent (не ниже **25.12.13**). В конструкторе по умолчанию используется **25.12.14**; при необходимости передайте `appVersion: '25.21.3'` или новее.
150
+
151
+ Если сервер отвечает **`qr_login.disabled`**, проверьте версию приложения в опциях, откройте [web.max.ru](https://web.max.ru) в браузере или войдите на втором устройстве по номеру телефона.
152
+
129
153
  #### Способ 3: Token авторизация
130
154
 
131
155
  Если у вас уже есть токен (от другого сервиса/приложения):
@@ -172,15 +196,39 @@ const client = new WebMaxClient({
172
196
  name: 'session', // Имя сессии (для сохранения авторизации)
173
197
  token: 'An_Sx6H...', // Токен авторизации (опционально)
174
198
  configPath: 'myconfig', // Путь к config файлу (опционально)
175
- deviceType: 'WEB', // Тип устройства: 'WEB', 'IOS', 'ANDROID' (опционально)
199
+ deviceType: 'WEB', // Тип устройства: 'WEB', 'IOS', 'ANDROID', 'DESKTOP' (опционально)
176
200
  saveToken: true, // Сохранять токен в сессию (по умолчанию true)
177
- debug: false, // Отладочный режим (опционально)
201
+ debug: false, // TCP/WebSocket: краткий лог opcode; также учитывается WEBMAX_DEBUG=1, DEBUG=1
202
+ // Лог входящих (JSON в консоль), см. ниже:
203
+ logIncoming: undefined, // false | 'messages' | 'verbose' — по умолчанию см. таблицу
204
+ logIncomingVerbose: false, // явно включить verbose (как logIncoming: 'verbose')
178
205
  apiUrl: 'wss://...', // URL WebSocket API (опционально)
179
206
  maxReconnectAttempts: 5,// Максимальное количество попыток переподключения
180
- reconnectDelay: 3000 // Задержка между попытками переподключения (мс)
207
+ reconnectDelay: 3000, // Задержка между попытками переподключения (мс)
208
+ // User-Agent / клиент (важно для GET_QR, см. showLinkDeviceQR):
209
+ appVersion: '25.12.14', // Рекомендуется ≥ 25.12.13 для запроса QR
210
+ ua: 'Mozilla/5.0 ...', // или headerUserAgent
211
+ osVersion: 'Windows 11',
212
+ screen: '1920x1080 1.0x',
213
+ timezone: 'Europe/Moscow',
214
+ locale: 'ru',
215
+ buildNumber: 0x97cb, // опционально
216
+ clientSessionId: 1 // опционально
181
217
  });
182
218
  ```
183
219
 
220
+ **Лог входящих (`logIncoming`):** печать блоков `📥 [incoming:…]` с JSON.
221
+
222
+ | Значение / условие | Поведение |
223
+ |--------------------|-----------|
224
+ | `logIncoming: false` | Выкл. |
225
+ | `logIncoming: 'messages'` | Только входящие сообщения (`message.rawData`). |
226
+ | `logIncoming: 'verbose'` или `logIncomingVerbose: true` | Сообщения + `connected`, `raw_message`, `message_removed`, `chat_action`, `error`. |
227
+ | не указано | По умолчанию: как `'messages'`; если **`WEBMAX_DEBUG=1`** — как `'verbose'`. Явное **`logIncoming: 'messages'`** отключает расширенный режим даже при `WEBMAX_DEBUG`. |
228
+ | **`WEBMAX_SILENT=1`** | Выкл. всех дампов. |
229
+
230
+ Ручной вывод в том же формате: `client.logIncoming('my_label', data)`. Текущий режим: `client.incomingLogMode` (`'off' \| 'messages' \| 'verbose'`). Низкоуровнево: `resolveIncomingLogMode(options)`, `printIncomingLog(label, payload)` из пакета.
231
+
184
232
  #### Методы
185
233
 
186
234
  ##### `start()`
@@ -206,6 +254,24 @@ const authSession = await client.authorizeBySMS('+79001234567');
206
254
  await authSession.sendCode('123456');
207
255
  ```
208
256
 
257
+ ##### `showLinkDeviceQR(options)`
258
+
259
+ Показать в консоли **QR-код для привязки устройства** (как в приложении Max: телефон сканирует QR). Нужна **уже выполненная авторизация** (`start()` или `connect` + `sync`).
260
+
261
+ - Для **WEB** запрос выполняется по текущему WebSocket.
262
+ - Для **IOS/ANDROID** после входа по TCP используется **второе** WebSocket-подключение без повторного `LOGIN` на той сессии (иначе `GET_QR` на том же TCP недоступен).
263
+
264
+ ```javascript
265
+ await client.showLinkDeviceQR();
266
+ await client.showLinkDeviceQR({ waitForScan: false, small: false });
267
+ ```
268
+
269
+ Возвращает `Promise<{ qrLink, trackId, pollingInterval, expiresAt }>`.
270
+
271
+ ##### `requestQR()`, `checkQRStatus(trackId)`, `loginByQR(trackId)`, `authorizeByQR()`
272
+
273
+ Низкоуровневые шаги QR-авторизации для **WEB** (первый вход без SMS). Обычно достаточно `start()` без токена или `authorizeByQR()`.
274
+
209
275
  ##### `sendMessage(options)`
210
276
 
211
277
  Отправляет сообщение в чат с уведомлением (notify: true).
@@ -214,7 +280,7 @@ await authSession.sendCode('123456');
214
280
  const message = await client.sendMessage({
215
281
  chatId: 123,
216
282
  text: 'Привет!',
217
- cid: Date.now(),
283
+ // cid опционально; на TCP не используйте Date.now() (нужен int32)
218
284
  replyTo: null, // ID сообщения для ответа (опционально)
219
285
  attachments: [] // Вложения (опционально)
220
286
  });
@@ -228,7 +294,6 @@ const message = await client.sendMessage({
228
294
  const message = await client.sendMessageChannel({
229
295
  chatId: 123,
230
296
  text: 'Сообщение в канал',
231
- cid: Date.now(),
232
297
  replyTo: null, // ID сообщения для ответа (опционально)
233
298
  attachments: [] // Вложения (опционально)
234
299
  });
@@ -391,13 +456,11 @@ client.onError(async (error) => {
391
456
 
392
457
  ##### `reply(options)`
393
458
 
394
- Отвечает на сообщение.
459
+ Отправляет текст **в тот же чат**. По умолчанию **без** цитаты исходного сообщения (`link REPLY`), т.к. на TCP-сокете сервер часто возвращает «Ошибка валидации» для ответа-цитаты. Чтобы попробовать ответ с цитатой: `{ text: '...', quote: true }`.
395
460
 
396
461
  ```javascript
397
- await message.reply({
398
- text: 'Ответ на сообщение',
399
- cid: Date.now()
400
- });
462
+ await message.reply({ text: 'Ответ на сообщение' });
463
+ await message.reply({ text: '...', quote: true });
401
464
  ```
402
465
 
403
466
  ##### `edit(options)`
@@ -426,6 +489,59 @@ await message.delete();
426
489
  await message.forward(789);
427
490
  ```
428
491
 
492
+ ##### `downloadAttachment(index, options?)`
493
+
494
+ Скачивает вложение по полю **`baseUrl`** (или `url`) из `message.attachments[index]` в **временный файл**. По умолчанию каталог — `os.tmpdir()` (на Windows обычно `%TEMP%`). Имя файла генерируется автоматически; расширение берётся из заголовка `Content-Type`, при необходимости — из типа вложения (`_type`, например `PHOTO`).
495
+
496
+ Возвращает `{ path, contentType }`.
497
+
498
+ ```javascript
499
+ if (message.attachments.length) {
500
+ const { path, contentType } = await message.downloadAttachment(0);
501
+ console.log('Сохранено:', path, contentType);
502
+ // после обработки можно удалить: fs.unlinkSync(path)
503
+ }
504
+
505
+ // Свой каталог или имя файла:
506
+ await message.downloadAttachment(0, {
507
+ dir: './downloads',
508
+ filename: 'photo.webp'
509
+ });
510
+ ```
511
+
512
+ ### Утилиты скачивания медиа / Media download helpers
513
+
514
+ Экспортируются из пакета наряду с `WebMaxClient`:
515
+
516
+ ```javascript
517
+ const {
518
+ downloadUrlToTempFile,
519
+ extFromContentType,
520
+ extFromAttachType
521
+ } = require('webmaxsocket');
522
+ ```
523
+
524
+ ##### `downloadUrlToTempFile(url, options?)`
525
+
526
+ HTTP(S)-запрос с следованием редиректам, запись тела ответа в файл.
527
+
528
+ | Опция | Описание |
529
+ |--------|----------|
530
+ | `dir` | Каталог (по умолчанию `os.tmpdir()`) |
531
+ | `filename` | Имя файла (только basename); если не задано — `max-media-<time>-<random>.<ext>` |
532
+ | `extFallback` | Расширение, если по `Content-Type` определить не удалось (например `'.jpg'`) |
533
+
534
+ ```javascript
535
+ const { path, contentType } = await downloadUrlToTempFile(
536
+ 'https://i.oneme.ru/i?r=...',
537
+ { extFallback: '.jpg' }
538
+ );
539
+ ```
540
+
541
+ ##### `extFromContentType(contentType)` / `extFromAttachType(attachType)`
542
+
543
+ Вспомогательные функции для подбора расширения по MIME или по `_type` вложения (`PHOTO`, `VIDEO`, …).
544
+
429
545
  ### User
430
546
 
431
547
  Класс, представляющий пользователя.
@@ -532,6 +648,10 @@ node example-ios.js
532
648
  node example-ios.js --debug
533
649
  ```
534
650
 
651
+ ### Пример 5: QR для второго устройства после SMS
652
+
653
+ После успешного `start()` с сохранённой сессией IOS/Android вызовите `showLinkDeviceQR()` (см. раздел **«QR после входа»** выше).
654
+
535
655
  ## Структура проекта
536
656
 
537
657
  ```
@@ -543,6 +663,8 @@ webmaxsocket/
543
663
  │ ├── userAgent.js # UserAgent генератор
544
664
  │ ├── opcodes.js # Протокол опкоды
545
665
  │ ├── constants.js # Константы
666
+ │ ├── downloadMedia.js # Скачивание медиа по URL во временный файл
667
+ │ ├── incomingLog.js # Режим logIncoming / печать входящих
546
668
  │ └── entities/
547
669
  │ ├── User.js # Класс пользователя
548
670
  │ ├── Message.js # Класс сообщения
@@ -580,8 +702,7 @@ const client2 = new WebMaxClient({ name: 'account1' }); // phone не требу
580
702
  try {
581
703
  const message = await client.sendMessage({
582
704
  chatId: 123,
583
- text: 'Привет!',
584
- cid: Date.now()
705
+ text: 'Привет!'
585
706
  });
586
707
  } catch (error) {
587
708
  console.error('Ошибка:', error.message);
@@ -609,11 +730,17 @@ DEBUG=1 node example.js
609
730
 
610
731
  1. **TCP Socket после QR-авторизации:** После первой успешной QR-авторизации клиент автоматически сохраняет `clientSessionId` и переключается на TCP Socket транспорт при следующем запуске для повышения стабильности.
611
732
 
612
- 2. **Разница между sendMessage и sendMessageChannel:**
733
+ 2. **QR для нового устройства после входа по SMS/TCP:** Используйте `showLinkDeviceQR()`. Это не отдельный опкод в протоколе, а тот же `GET_QR`, что и у веб-клиента; для уже залогиненного TCP-сокета запрос выполняется через **эфемерное WebSocket-подключение** (временный файл сессии `_link_qr_*` удаляется после завершения).
734
+
735
+ 3. **Версия `appVersion` и QR:** Слишком старая версия в User-Agent может привести к ответу `qr_login.disabled` на `GET_QR`. Задайте в конструкторе актуальную строку (по умолчанию **25.12.14**).
736
+
737
+ 4. **Разница между sendMessage и sendMessageChannel:**
613
738
  - `sendMessage()` - отправка с уведомлением (notify: true) для обычных чатов
614
739
  - `sendMessageChannel()` - отправка без уведомления (notify: false) для каналов
615
740
 
616
- 3. **Автоматический выбор транспорта:** Клиент автоматически определяет какой транспорт использовать на основе `deviceType` в сессии или config файле.
741
+ 5. **Автоматический выбор транспорта:** Клиент автоматически определяет какой транспорт использовать на основе `deviceType` в сессии или config файле.
742
+
743
+ 6. **`cid` при отправке сообщений (TCP/Socket):** сервер проверяет **signed int32**. Не передавайте `Date.now()` (миллисекунды ~1e12) — будет «Ошибка валидации». Либо не указывайте `cid` (клиент подставит свой), либо передайте целое в диапазоне **−2³¹ … 2³¹−1**.
617
744
 
618
745
  ## 🔗 Ссылки / Links
619
746
 
package/index.js CHANGED
@@ -11,6 +11,8 @@ const { User, Message, ChatAction } = require('./lib/entities');
11
11
  const { ChatActions, EventTypes, MessageTypes } = require('./lib/constants');
12
12
  const { Opcode, getOpcodeName } = require('./lib/opcodes');
13
13
  const { UserAgentPayload } = require('./lib/userAgent');
14
+ const { downloadUrlToTempFile, extFromContentType, extFromAttachType } = require('./lib/downloadMedia');
15
+ const { resolveIncomingLogMode, printIncomingLog } = require('./lib/incomingLog');
14
16
 
15
17
  module.exports = {
16
18
  WebMaxClient,
@@ -23,6 +25,11 @@ module.exports = {
23
25
  MessageTypes,
24
26
  Opcode,
25
27
  getOpcodeName,
26
- UserAgentPayload
28
+ UserAgentPayload,
29
+ downloadUrlToTempFile,
30
+ extFromContentType,
31
+ extFromAttachType,
32
+ resolveIncomingLogMode,
33
+ printIncomingLog
27
34
  };
28
35
 
package/lib/client.js CHANGED
@@ -10,6 +10,7 @@ const { Message, ChatAction, User } = require('./entities');
10
10
  const { EventTypes, ChatActions } = require('./constants');
11
11
  const { Opcode, DeviceType, getOpcodeName } = require('./opcodes');
12
12
  const { UserAgentPayload } = require('./userAgent');
13
+ const { resolveIncomingLogMode, printIncomingLog } = require('./incomingLog');
13
14
 
14
15
  /**
15
16
  * Загружает конфиг: { token, agent }
@@ -30,6 +31,30 @@ function loadSessionConfig(configPath) {
30
31
  return JSON.parse(data);
31
32
  }
32
33
 
34
+ /**
35
+ * Понятная ошибка при отказе сервера отдать QR (часто qr_login.disabled для неофициального WEB-handshake).
36
+ */
37
+ function throwIfGetQRRejected(payload) {
38
+ if (!payload || !payload.error) {
39
+ return;
40
+ }
41
+ const err = payload.error;
42
+ const text =
43
+ typeof err === 'string'
44
+ ? err
45
+ : err && typeof err.message === 'string'
46
+ ? err.message
47
+ : JSON.stringify(err);
48
+ if (String(text).includes('qr_login.disabled')) {
49
+ throw new Error(
50
+ 'Сервер Max отказал в выдаче QR (qr_login.disabled). Частые причины: устаревший appVersion в User-Agent (нужно ≥ 25.12.13), ' +
51
+ 'или отключение QR для данного клиента на стороне VK. Проверьте https://web.max.ru в браузере. ' +
52
+ 'Второй телефон к аккаунту можно добавить и обычным входом по номеру в приложении Max.'
53
+ );
54
+ }
55
+ throw new Error(`QR request error: ${text}`);
56
+ }
57
+
33
58
  /**
34
59
  * Основной клиент для работы с API Max
35
60
  */
@@ -63,17 +88,27 @@ class WebMaxClient extends EventEmitter {
63
88
  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
89
  const webDefaults = {
65
90
  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
91
+ locale: options.locale || configObj.locale || 'ru',
92
+ deviceLocale: options.deviceLocale || configObj.deviceLocale || configObj.locale || 'ru',
93
+ osVersion:
94
+ options.osVersion ||
95
+ configObj.osVersion ||
96
+ (deviceType === 'IOS' ? '18.6.2' : deviceType === 'ANDROID' ? '14' : 'Windows 11'),
97
+ deviceName:
98
+ options.deviceName ||
99
+ configObj.deviceName ||
100
+ (deviceType === 'IOS' ? 'Safari' : deviceType === 'ANDROID' ? 'Chrome' : 'Chrome'),
101
+ headerUserAgent: options.headerUserAgent || options.ua || uaString,
102
+ // Ниже 25.12.13 сервер может отвечать qr_login.disabled на GET_QR (см. PyMax _login).
103
+ appVersion: options.appVersion || configObj.appVersion || '25.12.14',
104
+ screen:
105
+ options.screen ||
106
+ configObj.screen ||
107
+ (deviceType === 'IOS' ? '390x844 3.0x' : deviceType === 'ANDROID' ? '360x780 3.0x' : '1080x1920 1.0x'),
108
+ timezone: options.timezone || configObj.timezone || 'Europe/Moscow',
109
+ buildNumber: options.buildNumber ?? configObj.buildNumber,
110
+ clientSessionId: options.clientSessionId ?? configObj.clientSessionId ?? this.session.get('clientSessionId'),
111
+ release: options.release ?? configObj.release
77
112
  };
78
113
  this._handshakeUserAgent = new UserAgentPayload(webDefaults);
79
114
  this.userAgent = this._handshakeUserAgent;
@@ -110,7 +145,39 @@ class WebMaxClient extends EventEmitter {
110
145
 
111
146
  this.messageQueue = [];
112
147
  this.pendingRequests = new Map();
113
- this.debug = options.debug || process.env.DEBUG === '1';
148
+ this.debug =
149
+ Boolean(options.debug) ||
150
+ process.env.DEBUG === '1' ||
151
+ process.env.WEBMAX_DEBUG === '1';
152
+ /** Режим JSON-лога входящих: off | messages | verbose (см. logIncoming в README) */
153
+ this._incomingLogMode = resolveIncomingLogMode(options);
154
+ this._wireIncomingLogListeners();
155
+ /** client id локальных исходящих сообщений (int32, часто ждут валидацию на сервере) */
156
+ this._clientSendCid = 1 + Math.floor(Math.random() * 0xfffff);
157
+ }
158
+
159
+ /**
160
+ * Текущий режим лога входящих: `off` | `messages` | `verbose`.
161
+ */
162
+ get incomingLogMode() {
163
+ return this._incomingLogMode;
164
+ }
165
+
166
+ /**
167
+ * Ручной вывод в формате `[incoming:label]` (как внутренний лог).
168
+ */
169
+ logIncoming(label, payload) {
170
+ printIncomingLog(label, payload);
171
+ }
172
+
173
+ _wireIncomingLogListeners() {
174
+ if (this._incomingLogMode !== 'verbose') return;
175
+ this.once('connected', () => {
176
+ printIncomingLog('connected', { event: 'connected' });
177
+ });
178
+ this.on('raw_message', (data) => {
179
+ printIncomingLog('raw_message', data);
180
+ });
114
181
  }
115
182
 
116
183
  /**
@@ -247,11 +314,9 @@ class WebMaxClient extends EventEmitter {
247
314
  console.log('Запрос QR-кода для авторизации...');
248
315
 
249
316
  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
-
317
+
318
+ throwIfGetQRRejected(response.payload);
319
+
255
320
  return response.payload;
256
321
  }
257
322
 
@@ -315,6 +380,127 @@ class WebMaxClient extends EventEmitter {
315
380
  }
316
381
  }
317
382
 
383
+ /**
384
+ * Вывести в консоль QR-код для привязки нового устройства (тот же поток, что и веб-вход).
385
+ * Требуется уже авторизованная сессия (после SMS/QR и sync).
386
+ * На телефоне: Профиль → Устройства / Безопасность → Подключить устройство (QR).
387
+ *
388
+ * @param {object} [options]
389
+ * @param {boolean} [options.waitForScan=true] — ждать, пока QR отсканируют
390
+ * @param {boolean} [options.small=true] — компактный QR в терминале
391
+ * @returns {Promise<{ qrLink: string, trackId: string, pollingInterval: number, expiresAt: number }>}
392
+ */
393
+ async showLinkDeviceQR(options = {}) {
394
+ const { waitForScan = true, small = true } = options;
395
+
396
+ if (!this.isConnected) {
397
+ throw new Error('Нет соединения: сначала await client.connect()');
398
+ }
399
+ if (!this.isAuthorized) {
400
+ throw new Error('Нужна авторизация: войдите в аккаунт и выполните sync, затем вызывайте showLinkDeviceQR');
401
+ }
402
+
403
+ // После LOGIN по TCP сервер не принимает GET_QR («Недопустимое состояние сессии») — тот же QR, что в веб-клиенте, только до авторизации по WebSocket.
404
+ if (this._useSocketTransport) {
405
+ return await this._showLinkDeviceQRViaEphemeralWeb(options);
406
+ }
407
+
408
+ console.log('Запрос QR-кода для привязки устройства...');
409
+ const response = await this.sendAndWait(Opcode.GET_QR, {});
410
+
411
+ throwIfGetQRRejected(response.payload);
412
+
413
+ const qrData = response.payload;
414
+ if (!qrData.qrLink || !qrData.trackId || !qrData.pollingInterval || !qrData.expiresAt) {
415
+ throw new Error('Неполные данные QR-кода от сервера');
416
+ }
417
+
418
+ await this._printLinkDeviceQRConsole(qrData.qrLink, small);
419
+ console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
420
+ console.log('='.repeat(70) + '\n');
421
+
422
+ if (waitForScan) {
423
+ await this.pollQRStatus(qrData.trackId, qrData.pollingInterval, qrData.expiresAt);
424
+ console.log('\n✅ Устройство подключено. Проверьте вход на телефоне.');
425
+ }
426
+
427
+ return {
428
+ qrLink: qrData.qrLink,
429
+ trackId: qrData.trackId,
430
+ pollingInterval: qrData.pollingInterval,
431
+ expiresAt: qrData.expiresAt
432
+ };
433
+ }
434
+
435
+ _printLinkDeviceQRConsole(qrLink, small = true) {
436
+ console.log('\n' + '='.repeat(70));
437
+ console.log('📱 ПРИВЯЗКА НОВОГО УСТРОЙСТВА');
438
+ console.log('='.repeat(70));
439
+ console.log('\nНа телефоне откройте Max — как при добавлении устройства в приложении:');
440
+ console.log('➡️ Профиль → Устройства / Безопасность → Подключить устройство (вход по QR)');
441
+ console.log('📸 Наведите камеру на QR ниже — это тот же поток, что у веб-клиента:\n');
442
+ return new Promise((resolve) => {
443
+ qrcode.generate(qrLink, { small }, (qrCode) => {
444
+ console.log(qrCode);
445
+ resolve();
446
+ });
447
+ });
448
+ }
449
+
450
+ /**
451
+ * QR для привязки устройства при основой сессии на TCP (IOS/ANDROID): кратковременный WEB-клиент без LOGIN.
452
+ */
453
+ async _showLinkDeviceQRViaEphemeralWeb(options = {}) {
454
+ const { waitForScan = true, small = true } = options;
455
+ const ephemeralName = `_link_qr_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
456
+ const webQr = new this.constructor({
457
+ name: ephemeralName,
458
+ deviceType: 'WEB',
459
+ debug: this.debug,
460
+ apiUrl: this.apiUrl,
461
+ origin: this.origin,
462
+ maxReconnectAttempts: 0
463
+ });
464
+
465
+ try {
466
+ console.log(
467
+ 'Отдельное WebSocket-подключение (как у web.max.ru): на уже залогиненном TCP запрос QR другим способом недоступен.'
468
+ );
469
+ await webQr.connect();
470
+
471
+ const response = await webQr.sendAndWait(Opcode.GET_QR, {});
472
+ throwIfGetQRRejected(response.payload);
473
+
474
+ const qrData = response.payload;
475
+ if (!qrData.qrLink || !qrData.trackId || !qrData.pollingInterval || !qrData.expiresAt) {
476
+ throw new Error('Неполные данные QR-кода от сервера');
477
+ }
478
+
479
+ await this._printLinkDeviceQRConsole(qrData.qrLink, small);
480
+
481
+ console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
482
+ console.log('='.repeat(70) + '\n');
483
+
484
+ if (waitForScan) {
485
+ await webQr.pollQRStatus(qrData.trackId, qrData.pollingInterval, qrData.expiresAt);
486
+ await webQr.loginByQR(qrData.trackId);
487
+ console.log('\n✅ Устройство подключено. Проверьте телефон.');
488
+ }
489
+
490
+ return {
491
+ qrLink: qrData.qrLink,
492
+ trackId: qrData.trackId,
493
+ pollingInterval: qrData.pollingInterval,
494
+ expiresAt: qrData.expiresAt
495
+ };
496
+ } finally {
497
+ try {
498
+ await webQr.stop();
499
+ webQr.session.destroy();
500
+ } catch (_) {}
501
+ }
502
+ }
503
+
318
504
  /**
319
505
  * Авторизация через QR-код
320
506
  */
@@ -331,13 +517,15 @@ class WebMaxClient extends EventEmitter {
331
517
  console.log('\n' + '='.repeat(70));
332
518
  console.log('🔐 АВТОРИЗАЦИЯ ЧЕРЕЗ QR-КОД');
333
519
  console.log('='.repeat(70));
334
- console.log('\n📱 Откройте приложение Max на телефоне');
335
- console.log('➡️ Настройки → Устройства → Подключить устройство');
520
+ console.log('\n📱 На телефоне: Профиль Устройства / Безопасность → Подключить устройство');
336
521
  console.log('📸 Отсканируйте QR-код ниже:\n');
337
522
 
338
- // Отображаем QR-код в консоли
339
- qrcode.generate(qrData.qrLink, { small: true }, (qrCode) => {
340
- console.log(qrCode);
523
+ // Отображаем QR-код в консоли (ждём вывод, затем опрос статуса)
524
+ await new Promise((resolve) => {
525
+ qrcode.generate(qrData.qrLink, { small: true }, (qrCode) => {
526
+ console.log(qrCode);
527
+ resolve();
528
+ });
341
529
  });
342
530
 
343
531
  console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
@@ -846,12 +1034,16 @@ class WebMaxClient extends EventEmitter {
846
1034
  }
847
1035
 
848
1036
  const message = new Message(messageData, this);
849
-
1037
+
1038
+ if (this._incomingLogMode === 'messages' || this._incomingLogMode === 'verbose') {
1039
+ printIncomingLog('message', message.rawData);
1040
+ }
1041
+
850
1042
  // Попытка загрузить информацию об отправителе если её нет
851
1043
  if (!message.sender && message.senderId && message.senderId !== this.me?.id) {
852
1044
  await message.fetchSender();
853
1045
  }
854
-
1046
+
855
1047
  await this.triggerHandlers(EventTypes.MESSAGE, message);
856
1048
  }
857
1049
 
@@ -860,6 +1052,9 @@ class WebMaxClient extends EventEmitter {
860
1052
  */
861
1053
  async handleRemovedMessage(data) {
862
1054
  const message = new Message(data, this);
1055
+ if (this._incomingLogMode === 'verbose') {
1056
+ printIncomingLog('message_removed', message.rawData);
1057
+ }
863
1058
  await this.triggerHandlers(EventTypes.MESSAGE_REMOVED, message);
864
1059
  }
865
1060
 
@@ -868,6 +1063,9 @@ class WebMaxClient extends EventEmitter {
868
1063
  */
869
1064
  async handleChatAction(data) {
870
1065
  const action = new ChatAction(data, this);
1066
+ if (this._incomingLogMode === 'verbose') {
1067
+ printIncomingLog('chat_action', action.rawData);
1068
+ }
871
1069
  await this.triggerHandlers(EventTypes.CHAT_ACTION, action);
872
1070
  }
873
1071
 
@@ -919,6 +1117,73 @@ class WebMaxClient extends EventEmitter {
919
1117
  });
920
1118
  }
921
1119
 
1120
+ _nextClientMessageId() {
1121
+ const n = this._clientSendCid;
1122
+ this._clientSendCid = (this._clientSendCid % 0x7fffffff) + 1;
1123
+ return n;
1124
+ }
1125
+
1126
+ /** int32: Date.now() и большие числа ломают валидацию MSG_SEND на TCP */
1127
+ _normalizeOutgoingCid(cid) {
1128
+ if (cid == null || cid === '') return this._nextClientMessageId();
1129
+ const n = Number(cid);
1130
+ if (!Number.isFinite(n)) return this._nextClientMessageId();
1131
+ const x = Math.trunc(n);
1132
+ if (x > 2147483647 || x < -2147483648) {
1133
+ return this._nextClientMessageId();
1134
+ }
1135
+ return x;
1136
+ }
1137
+
1138
+ /**
1139
+ * messageId для REPLY: int, если безопасно, иначе строка (длинные id).
1140
+ */
1141
+ _normalizeReplyMessageId(replyTo) {
1142
+ if (replyTo == null || replyTo === '') return null;
1143
+ if (typeof replyTo === 'number' && Number.isFinite(replyTo)) return replyTo;
1144
+ if (typeof replyTo === 'bigint') return Number(replyTo);
1145
+ if (typeof replyTo === 'string' && /^-?\d+$/.test(replyTo)) {
1146
+ const n = Number(replyTo);
1147
+ if (Number.isSafeInteger(n)) return n;
1148
+ return replyTo;
1149
+ }
1150
+ return String(replyTo);
1151
+ }
1152
+
1153
+ /**
1154
+ * Собирает тело message для MSG_SEND: без link: null; cid в int32; elements для текста.
1155
+ */
1156
+ _buildOutgoingMessageBody(text, cid, replyTo, attachments) {
1157
+ const t = text == null ? '' : String(text);
1158
+ const cidVal = this._normalizeOutgoingCid(cid);
1159
+
1160
+ const body = {
1161
+ text: t,
1162
+ cid: cidVal,
1163
+ elements: []
1164
+ };
1165
+
1166
+ const att = attachments || [];
1167
+ if (att.length) {
1168
+ body.attaches = att;
1169
+ }
1170
+
1171
+ if (replyTo != null && replyTo !== '') {
1172
+ body.link = {
1173
+ type: 'REPLY',
1174
+ messageId: this._normalizeReplyMessageId(replyTo)
1175
+ };
1176
+ }
1177
+ return body;
1178
+ }
1179
+
1180
+ _normalizeChatId(chatId) {
1181
+ if (chatId == null) return chatId;
1182
+ if (typeof chatId === 'bigint') return Number(chatId);
1183
+ const n = Number(chatId);
1184
+ return Number.isNaN(n) ? chatId : n;
1185
+ }
1186
+
922
1187
  /**
923
1188
  * Отправка сообщения (с уведомлением)
924
1189
  */
@@ -930,14 +1195,8 @@ class WebMaxClient extends EventEmitter {
930
1195
  const { chatId, text, cid, replyTo, attachments } = options;
931
1196
 
932
1197
  const payload = {
933
- chatId: chatId,
934
- message: {
935
- text: text || '',
936
- cid: cid || Date.now(),
937
- elements: [],
938
- attaches: attachments || [],
939
- link: replyTo ? { type: 'REPLY', messageId: replyTo } : null
940
- },
1198
+ chatId: this._normalizeChatId(chatId),
1199
+ message: this._buildOutgoingMessageBody(text, cid, replyTo, attachments),
941
1200
  notify: true
942
1201
  };
943
1202
 
@@ -961,14 +1220,8 @@ class WebMaxClient extends EventEmitter {
961
1220
  const { chatId, text, cid, replyTo, attachments } = options;
962
1221
 
963
1222
  const payload = {
964
- chatId: chatId,
965
- message: {
966
- text: text || '',
967
- cid: cid || Date.now(),
968
- elements: [],
969
- attaches: attachments || [],
970
- link: replyTo ? { type: 'REPLY', messageId: replyTo } : null
971
- },
1223
+ chatId: this._normalizeChatId(chatId),
1224
+ message: this._buildOutgoingMessageBody(text, cid, replyTo, attachments),
972
1225
  notify: false
973
1226
  };
974
1227
 
@@ -1097,8 +1350,19 @@ class WebMaxClient extends EventEmitter {
1097
1350
  * Выполнение зарегистрированных обработчиков
1098
1351
  */
1099
1352
  async triggerHandlers(eventType, data = null) {
1353
+ if (
1354
+ eventType === EventTypes.ERROR &&
1355
+ data !== null &&
1356
+ this._incomingLogMode === 'verbose'
1357
+ ) {
1358
+ printIncomingLog('error', {
1359
+ message: data && data.message,
1360
+ stack: data && data.stack
1361
+ });
1362
+ }
1363
+
1100
1364
  const handlers = this.handlers[eventType] || [];
1101
-
1365
+
1102
1366
  for (const handler of handlers) {
1103
1367
  try {
1104
1368
  if (data !== null) {
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Скачивание медиа по публичному URL (например baseUrl из attaches) во временный файл.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const https = require('https');
9
+ const http = require('http');
10
+ const { pipeline } = require('stream/promises');
11
+
12
+ const UA = 'Mozilla/5.0 (compatible; WebMaxSocket/1.1)';
13
+
14
+ const CONTENT_TYPE_EXT = {
15
+ 'image/jpeg': '.jpg',
16
+ 'image/jpg': '.jpg',
17
+ 'image/png': '.png',
18
+ 'image/webp': '.webp',
19
+ 'image/gif': '.gif',
20
+ 'video/mp4': '.mp4',
21
+ 'video/webm': '.webm',
22
+ 'application/octet-stream': '.bin'
23
+ };
24
+
25
+ function extFromContentType(ct) {
26
+ if (!ct) return '';
27
+ const main = String(ct).split(';')[0].trim().toLowerCase();
28
+ return CONTENT_TYPE_EXT[main] || '';
29
+ }
30
+
31
+ function extFromAttachType(t) {
32
+ if (!t) return '';
33
+ const u = String(t).toUpperCase();
34
+ if (u === 'PHOTO' || u === 'IMAGE') return '.jpg';
35
+ if (u === 'VIDEO') return '.mp4';
36
+ if (u === 'VOICE' || u === 'AUDIO') return '.ogg';
37
+ if (u === 'FILE') return '.bin';
38
+ return '';
39
+ }
40
+
41
+ /**
42
+ * @param {string} urlString
43
+ * @param {number} maxRedirects
44
+ * @returns {Promise<import('http').IncomingMessage>}
45
+ */
46
+ async function getFinalResponse(urlString, maxRedirects = 10) {
47
+ let url = String(urlString);
48
+ for (let i = 0; i < maxRedirects; i++) {
49
+ const res = await new Promise((resolve, reject) => {
50
+ const lib = url.startsWith('https') ? https : http;
51
+ const req = lib.request(
52
+ url,
53
+ { method: 'GET', headers: { 'User-Agent': UA } },
54
+ resolve
55
+ );
56
+ req.on('error', reject);
57
+ req.end();
58
+ });
59
+
60
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
61
+ res.resume();
62
+ url = new URL(res.headers.location, url).href;
63
+ continue;
64
+ }
65
+
66
+ if (res.statusCode !== 200) {
67
+ res.resume();
68
+ const err = new Error(`HTTP ${res.statusCode}`);
69
+ err.statusCode = res.statusCode;
70
+ throw err;
71
+ }
72
+
73
+ return res;
74
+ }
75
+ throw new Error('Too many redirects');
76
+ }
77
+
78
+ /**
79
+ * Скачивает URL во временный файл (по умолчанию каталог ОС: os.tmpdir()).
80
+ *
81
+ * @param {string} url
82
+ * @param {{ dir?: string, filename?: string, extFallback?: string }} [options]
83
+ * @returns {Promise<{ path: string, contentType: string }>}
84
+ */
85
+ async function downloadUrlToTempFile(url, options = {}) {
86
+ if (!url || typeof url !== 'string') {
87
+ throw new Error('downloadUrlToTempFile: нужен URL строкой');
88
+ }
89
+
90
+ const dir = options.dir != null ? String(options.dir) : os.tmpdir();
91
+ const res = await getFinalResponse(url);
92
+ const ct = (res.headers['content-type'] || '').trim();
93
+ let ext = extFromContentType(ct);
94
+ if (!ext && options.extFallback) {
95
+ ext = options.extFallback.startsWith('.')
96
+ ? options.extFallback
97
+ : `.${options.extFallback}`;
98
+ }
99
+ if (!ext) ext = '.bin';
100
+
101
+ const base =
102
+ options.filename ||
103
+ `max-media-${Date.now()}-${Math.random().toString(36).slice(2, 11)}${ext}`;
104
+ const safeName = path.basename(base);
105
+ const destPath = path.join(dir, safeName);
106
+
107
+ const ws = fs.createWriteStream(destPath);
108
+ try {
109
+ await pipeline(res, ws);
110
+ } catch (e) {
111
+ try {
112
+ await fs.promises.unlink(destPath);
113
+ } catch (_) {}
114
+ throw e;
115
+ }
116
+
117
+ return { path: destPath, contentType: ct };
118
+ }
119
+
120
+ module.exports = {
121
+ downloadUrlToTempFile,
122
+ extFromContentType,
123
+ extFromAttachType
124
+ };
@@ -1,4 +1,5 @@
1
1
  const User = require('./User');
2
+ const { downloadUrlToTempFile, extFromAttachType } = require('../downloadMedia');
2
3
 
3
4
  /**
4
5
  * Класс представляющий сообщение
@@ -6,7 +7,12 @@ const User = require('./User');
6
7
  class Message {
7
8
  constructor(data, client) {
8
9
  this.client = client;
9
- this.id = data.id || data.messageId || null;
10
+ this.id =
11
+ data.id ??
12
+ data.messageId ??
13
+ data.message_id ??
14
+ (data.message && (data.message.id ?? data.message.messageId)) ??
15
+ null;
10
16
  this.cid = data.cid || null;
11
17
  this.chatId = data.chatId || data.chat_id || null;
12
18
 
@@ -68,20 +74,26 @@ class Message {
68
74
  }
69
75
 
70
76
  /**
71
- * Ответить на сообщение
77
+ * Отправить текст в этот же чат (как ответ собеседнику).
78
+ * По умолчанию без цитаты (без link REPLY): на TCP-сокете сервер часто отклоняет MSG_SEND с link.
79
+ * Цитата/ветка: reply({ text, quote: true }) — если сервер вернёт ошибку, оставьте quote: false.
72
80
  */
73
81
  async reply(options) {
74
82
  if (typeof options === 'string') {
75
83
  options = { text: options };
76
84
  }
77
85
 
78
- return await this.client.sendMessage({
86
+ const { text, cid, attachments, quote = false } = options;
87
+ const payload = {
79
88
  chatId: this.chatId,
80
- text: options.text,
81
- cid: options.cid || Date.now(),
82
- replyTo: this.id,
83
- ...options
84
- });
89
+ text,
90
+ cid,
91
+ attachments
92
+ };
93
+ if (quote && this.id != null && this.id !== '') {
94
+ payload.replyTo = this.id;
95
+ }
96
+ return await this.client.sendMessage(payload);
85
97
  }
86
98
 
87
99
  /**
@@ -110,6 +122,27 @@ class Message {
110
122
  });
111
123
  }
112
124
 
125
+ /**
126
+ * Скачать вложение по его baseUrl (HTTPS) во временный файл.
127
+ * По умолчанию каталог: {@link https://nodejs.org/api/os.html#ostmpdir os.tmpdir()}.
128
+ *
129
+ * @param {number} [index=0] индекс в массиве attachments
130
+ * @param {{ dir?: string, filename?: string }} [options]
131
+ * @returns {Promise<{ path: string, contentType: string }>}
132
+ */
133
+ async downloadAttachment(index = 0, options = {}) {
134
+ const att = this.attachments[index];
135
+ if (!att) {
136
+ throw new Error(`Нет вложения с индексом ${index}`);
137
+ }
138
+ const url = att.baseUrl || att.url;
139
+ if (!url || typeof url !== 'string') {
140
+ throw new Error('У вложения нет baseUrl/url для скачивания');
141
+ }
142
+ const extFallback = extFromAttachType(att._type || att.type);
143
+ return downloadUrlToTempFile(url, { ...options, extFallback });
144
+ }
145
+
113
146
  /**
114
147
  * Переслать сообщение
115
148
  */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Режим логирования входящих событий для WebMaxClient.
3
+ */
4
+
5
+ /**
6
+ * @param {object} [options] опции конструктора WebMaxClient (`logIncoming`, `logIncomingVerbose`, …)
7
+ * @returns {{ mode: 'off' | 'messages' | 'verbose' }}
8
+ *
9
+ * - `logIncoming: false` — выкл.
10
+ * - `logIncoming: 'verbose'` или `logIncomingVerbose: true` — JSON сообщений + connected, raw_message, removed, chat_action, error.
11
+ * - `logIncoming: 'messages'` — только JSON входящих сообщений (даже при WEBMAX_DEBUG).
12
+ * - `WEBMAX_SILENT=1` — выкл. дампов.
13
+ * - `WEBMAX_DEBUG=1` — режим verbose, если не задано явно `logIncoming: 'messages'`.
14
+ */
15
+ function resolveIncomingLogMode(options = {}) {
16
+ const silent = process.env.WEBMAX_SILENT === '1';
17
+ const envVerbose = process.env.WEBMAX_DEBUG === '1';
18
+
19
+ const li = options.logIncoming;
20
+
21
+ if (li === false) {
22
+ return { mode: 'off' };
23
+ }
24
+ if (li === 'verbose' || options.logIncomingVerbose === true) {
25
+ return { mode: 'verbose' };
26
+ }
27
+ if (silent) {
28
+ return { mode: 'off' };
29
+ }
30
+ if (li === 'messages') {
31
+ return { mode: 'messages' };
32
+ }
33
+ if (envVerbose) {
34
+ return { mode: 'verbose' };
35
+ }
36
+ return { mode: 'messages' };
37
+ }
38
+
39
+ /**
40
+ * @param {string} label
41
+ * @param {unknown} payload
42
+ */
43
+ function printIncomingLog(label, payload) {
44
+ try {
45
+ const s = JSON.stringify(
46
+ payload,
47
+ (k, v) => (typeof v === 'bigint' ? v.toString() : v),
48
+ 2
49
+ );
50
+ console.log(`\n📥 [incoming:${label}]\n${s}\n`);
51
+ } catch (_) {
52
+ console.log(`\n📥 [incoming:${label}]`, payload, '\n');
53
+ }
54
+ }
55
+
56
+ module.exports = {
57
+ resolveIncomingLogMode,
58
+ printIncomingLog
59
+ };
@@ -63,6 +63,20 @@ function unpackPacket(data) {
63
63
  return { ver, cmd, seq, opcode, payload };
64
64
  }
65
65
 
66
+ /** JSON для логов: bigint → string, обрезка длинных строк */
67
+ function safeJsonForLog(obj, maxLen = 12000) {
68
+ try {
69
+ const s = JSON.stringify(
70
+ obj,
71
+ (k, v) => (typeof v === 'bigint' ? v.toString() : v),
72
+ 2
73
+ );
74
+ return s.length > maxLen ? `${s.slice(0, maxLen)}\n… [truncated ${s.length - maxLen} chars]` : s;
75
+ } catch (_) {
76
+ return String(obj);
77
+ }
78
+ }
79
+
66
80
  function readExactlyFromBuffer(transport, n) {
67
81
  return new Promise((resolve) => {
68
82
  const tryResolve = () => {
@@ -104,6 +118,10 @@ class MaxSocketTransport {
104
118
  if (this.debug) console.log('[Socket]', ...args);
105
119
  }
106
120
 
121
+ _debugErr(...args) {
122
+ if (this.debug) console.error('[Socket]', ...args);
123
+ }
124
+
107
125
  connect() {
108
126
  return new Promise((resolve, reject) => {
109
127
  const opts = {
@@ -180,9 +198,13 @@ class MaxSocketTransport {
180
198
  async sendAndWait(opcode, payload, cmd = 0, timeout = 20000) {
181
199
  if (!this.socket || this.socket.destroyed) throw new Error('Socket not connected');
182
200
 
183
- const msg = this._makeMessage(opcode, payload, cmd);
184
- const seqKey = msg.seq % 256;
185
- const packet = packPacket(msg.ver, msg.cmd, msg.seq, msg.opcode, msg.payload);
201
+ const outMsg = this._makeMessage(opcode, payload, cmd);
202
+ const seqKey = outMsg.seq % 256;
203
+ const packet = packPacket(outMsg.ver, outMsg.cmd, outMsg.seq, outMsg.opcode, outMsg.payload);
204
+
205
+ if (this.debug) {
206
+ this._log('→', getOpcodeName(opcode), `(seq=${outMsg.seq})`, safeJsonForLog(payload));
207
+ }
186
208
 
187
209
  let pendingRef;
188
210
  const promise = new Promise((resolve, reject) => {
@@ -212,8 +234,26 @@ class MaxSocketTransport {
212
234
 
213
235
  const result = await promise;
214
236
  if (result.payload && result.payload.error) {
215
- const errMsg = result.payload.localizedMessage || result.payload.error?.message || JSON.stringify(result.payload.error);
216
- throw new Error(errMsg);
237
+ const err = result.payload.error;
238
+ const localized = result.payload.localizedMessage;
239
+ const errText =
240
+ localized ||
241
+ (typeof err === 'string' ? err : err && err.message != null ? String(err.message) : '') ||
242
+ JSON.stringify(err);
243
+
244
+ if (this.debug) {
245
+ this._debugErr('RPC error response', {
246
+ opcode: getOpcodeName(opcode),
247
+ opcodeNum: opcode,
248
+ seq: outMsg.seq,
249
+ outgoingPayload: safeJsonForLog(payload),
250
+ fullResponsePayload: safeJsonForLog(result.payload)
251
+ });
252
+ }
253
+
254
+ const e = new Error(errText);
255
+ if (this.debug) e.rawPayload = result.payload;
256
+ throw e;
217
257
  }
218
258
  return result;
219
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webmaxsocket",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Node.js client for Max Messenger with QR code and token authentication",
5
5
  "main": "index.js",
6
6
  "scripts": {