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 +81 -12
- package/index.js +8 -1
- package/lib/client.js +129 -20
- package/lib/downloadMedia.js +124 -0
- package/lib/entities/Message.js +41 -8
- package/lib/incomingLog.js +59 -0
- package/lib/socketTransport.js +45 -5
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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 =
|
|
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
|
+
};
|
package/lib/entities/Message.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
86
|
+
const { text, cid, attachments, quote = false } = options;
|
|
87
|
+
const payload = {
|
|
79
88
|
chatId: this.chatId,
|
|
80
|
-
text
|
|
81
|
-
cid
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
};
|
package/lib/socketTransport.js
CHANGED
|
@@ -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
|
|
184
|
-
const seqKey =
|
|
185
|
-
const packet = packPacket(
|
|
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
|
|
216
|
-
|
|
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
|
}
|