webmaxsocket 1.1.1 → 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
@@ -13,9 +13,11 @@
13
13
  - ✅ **Автоматическое сохранение сессий** / Automatic session storage
14
14
  - ✅ **Автовыбор транспорта** после QR-авторизации (переход на TCP)
15
15
  - ✅ **Отправка и получение сообщений** / Send and receive messages
16
+ - ✅ **Скачивание вложений по URL** (`baseUrl` из `attaches`) во временный файл — `downloadUrlToTempFile`, `message.downloadAttachment()`
16
17
  - ✅ **Редактирование и удаление сообщений** / Edit and delete messages
17
18
  - ✅ **Event-driven архитектура** / Event-driven architecture
18
19
  - ✅ **Обработка входящих уведомлений** / Handle incoming notifications
20
+ - ✅ **Встроенный лог входящих** (`logIncoming`, `WEBMAX_DEBUG`, `WEBMAX_SILENT`) — JSON в консоль без ручных обработчиков
19
21
  - ✅ **TypeScript-ready** структура / TypeScript-ready structure
20
22
 
21
23
  ## 📦 Установка / Installation
@@ -62,8 +64,7 @@ async function main() {
62
64
 
63
65
  // Автоответ / Auto-reply
64
66
  await message.reply({
65
- text: `Привет! Я получил: "${message.text}"`,
66
- cid: Date.now()
67
+ text: `Привет! Я получил: "${message.text}"`
67
68
  });
68
69
  });
69
70
 
@@ -197,7 +198,10 @@ const client = new WebMaxClient({
197
198
  configPath: 'myconfig', // Путь к config файлу (опционально)
198
199
  deviceType: 'WEB', // Тип устройства: 'WEB', 'IOS', 'ANDROID', 'DESKTOP' (опционально)
199
200
  saveToken: true, // Сохранять токен в сессию (по умолчанию true)
200
- 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')
201
205
  apiUrl: 'wss://...', // URL WebSocket API (опционально)
202
206
  maxReconnectAttempts: 5,// Максимальное количество попыток переподключения
203
207
  reconnectDelay: 3000, // Задержка между попытками переподключения (мс)
@@ -213,6 +217,18 @@ const client = new WebMaxClient({
213
217
  });
214
218
  ```
215
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
+
216
232
  #### Методы
217
233
 
218
234
  ##### `start()`
@@ -264,7 +280,7 @@ await client.showLinkDeviceQR({ waitForScan: false, small: false });
264
280
  const message = await client.sendMessage({
265
281
  chatId: 123,
266
282
  text: 'Привет!',
267
- cid: Date.now(),
283
+ // cid опционально; на TCP не используйте Date.now() (нужен int32)
268
284
  replyTo: null, // ID сообщения для ответа (опционально)
269
285
  attachments: [] // Вложения (опционально)
270
286
  });
@@ -278,7 +294,6 @@ const message = await client.sendMessage({
278
294
  const message = await client.sendMessageChannel({
279
295
  chatId: 123,
280
296
  text: 'Сообщение в канал',
281
- cid: Date.now(),
282
297
  replyTo: null, // ID сообщения для ответа (опционально)
283
298
  attachments: [] // Вложения (опционально)
284
299
  });
@@ -441,13 +456,11 @@ client.onError(async (error) => {
441
456
 
442
457
  ##### `reply(options)`
443
458
 
444
- Отвечает на сообщение.
459
+ Отправляет текст **в тот же чат**. По умолчанию **без** цитаты исходного сообщения (`link REPLY`), т.к. на TCP-сокете сервер часто возвращает «Ошибка валидации» для ответа-цитаты. Чтобы попробовать ответ с цитатой: `{ text: '...', quote: true }`.
445
460
 
446
461
  ```javascript
447
- await message.reply({
448
- text: 'Ответ на сообщение',
449
- cid: Date.now()
450
- });
462
+ await message.reply({ text: 'Ответ на сообщение' });
463
+ await message.reply({ text: '...', quote: true });
451
464
  ```
452
465
 
453
466
  ##### `edit(options)`
@@ -476,6 +489,59 @@ await message.delete();
476
489
  await message.forward(789);
477
490
  ```
478
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
+
479
545
  ### User
480
546
 
481
547
  Класс, представляющий пользователя.
@@ -597,6 +663,8 @@ webmaxsocket/
597
663
  │ ├── userAgent.js # UserAgent генератор
598
664
  │ ├── opcodes.js # Протокол опкоды
599
665
  │ ├── constants.js # Константы
666
+ │ ├── downloadMedia.js # Скачивание медиа по URL во временный файл
667
+ │ ├── incomingLog.js # Режим logIncoming / печать входящих
600
668
  │ └── entities/
601
669
  │ ├── User.js # Класс пользователя
602
670
  │ ├── Message.js # Класс сообщения
@@ -634,8 +702,7 @@ const client2 = new WebMaxClient({ name: 'account1' }); // phone не требу
634
702
  try {
635
703
  const message = await client.sendMessage({
636
704
  chatId: 123,
637
- text: 'Привет!',
638
- cid: Date.now()
705
+ text: 'Привет!'
639
706
  });
640
707
  } catch (error) {
641
708
  console.error('Ошибка:', error.message);
@@ -673,6 +740,8 @@ DEBUG=1 node example.js
673
740
 
674
741
  5. **Автоматический выбор транспорта:** Клиент автоматически определяет какой транспорт использовать на основе `deviceType` в сессии или config файле.
675
742
 
743
+ 6. **`cid` при отправке сообщений (TCP/Socket):** сервер проверяет **signed int32**. Не передавайте `Date.now()` (миллисекунды ~1e12) — будет «Ошибка валидации». Либо не указывайте `cid` (клиент подставит свой), либо передайте целое в диапазоне **−2³¹ … 2³¹−1**.
744
+
676
745
  ## 🔗 Ссылки / Links
677
746
 
678
747
  - [GitHub Repository](https://github.com/Tellarion/webmaxsocket)
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 }
@@ -144,7 +145,39 @@ class WebMaxClient extends EventEmitter {
144
145
 
145
146
  this.messageQueue = [];
146
147
  this.pendingRequests = new Map();
147
- 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
+ });
148
181
  }
149
182
 
150
183
  /**
@@ -1001,12 +1034,16 @@ class WebMaxClient extends EventEmitter {
1001
1034
  }
1002
1035
 
1003
1036
  const message = new Message(messageData, this);
1004
-
1037
+
1038
+ if (this._incomingLogMode === 'messages' || this._incomingLogMode === 'verbose') {
1039
+ printIncomingLog('message', message.rawData);
1040
+ }
1041
+
1005
1042
  // Попытка загрузить информацию об отправителе если её нет
1006
1043
  if (!message.sender && message.senderId && message.senderId !== this.me?.id) {
1007
1044
  await message.fetchSender();
1008
1045
  }
1009
-
1046
+
1010
1047
  await this.triggerHandlers(EventTypes.MESSAGE, message);
1011
1048
  }
1012
1049
 
@@ -1015,6 +1052,9 @@ class WebMaxClient extends EventEmitter {
1015
1052
  */
1016
1053
  async handleRemovedMessage(data) {
1017
1054
  const message = new Message(data, this);
1055
+ if (this._incomingLogMode === 'verbose') {
1056
+ printIncomingLog('message_removed', message.rawData);
1057
+ }
1018
1058
  await this.triggerHandlers(EventTypes.MESSAGE_REMOVED, message);
1019
1059
  }
1020
1060
 
@@ -1023,6 +1063,9 @@ class WebMaxClient extends EventEmitter {
1023
1063
  */
1024
1064
  async handleChatAction(data) {
1025
1065
  const action = new ChatAction(data, this);
1066
+ if (this._incomingLogMode === 'verbose') {
1067
+ printIncomingLog('chat_action', action.rawData);
1068
+ }
1026
1069
  await this.triggerHandlers(EventTypes.CHAT_ACTION, action);
1027
1070
  }
1028
1071
 
@@ -1074,6 +1117,73 @@ class WebMaxClient extends EventEmitter {
1074
1117
  });
1075
1118
  }
1076
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
+
1077
1187
  /**
1078
1188
  * Отправка сообщения (с уведомлением)
1079
1189
  */
@@ -1085,14 +1195,8 @@ class WebMaxClient extends EventEmitter {
1085
1195
  const { chatId, text, cid, replyTo, attachments } = options;
1086
1196
 
1087
1197
  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
- },
1198
+ chatId: this._normalizeChatId(chatId),
1199
+ message: this._buildOutgoingMessageBody(text, cid, replyTo, attachments),
1096
1200
  notify: true
1097
1201
  };
1098
1202
 
@@ -1116,14 +1220,8 @@ class WebMaxClient extends EventEmitter {
1116
1220
  const { chatId, text, cid, replyTo, attachments } = options;
1117
1221
 
1118
1222
  const payload = {
1119
- chatId: chatId,
1120
- message: {
1121
- text: text || '',
1122
- cid: cid || Date.now(),
1123
- elements: [],
1124
- attaches: attachments || [],
1125
- link: replyTo ? { type: 'REPLY', messageId: replyTo } : null
1126
- },
1223
+ chatId: this._normalizeChatId(chatId),
1224
+ message: this._buildOutgoingMessageBody(text, cid, replyTo, attachments),
1127
1225
  notify: false
1128
1226
  };
1129
1227
 
@@ -1252,8 +1350,19 @@ class WebMaxClient extends EventEmitter {
1252
1350
  * Выполнение зарегистрированных обработчиков
1253
1351
  */
1254
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
+
1255
1364
  const handlers = this.handlers[eventType] || [];
1256
-
1365
+
1257
1366
  for (const handler of handlers) {
1258
1367
  try {
1259
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.1",
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": {