webmaxsocket 1.1.2 → 1.1.4
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 +89 -4
- package/api.package.md +193 -0
- package/lib/client.js +644 -4
- package/lib/opcodes.js +3 -0
- package/lib/socketTransport.js +10 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
**WebMaxSocket** — async Node.js библиотека для работы с внутренним API мессенджера Max. Поддерживает **QR-код авторизацию**, **Token авторизацию**, и работу через **WebSocket** (WEB) или **TCP Socket** (IOS/ANDROID).
|
|
6
6
|
|
|
7
|
+
Сводка всех методов: **[api.package.md](./api.package.md)**.
|
|
8
|
+
|
|
7
9
|
## ✨ Особенности / Features
|
|
8
10
|
|
|
9
11
|
- ✅ **QR-код авторизация** / QR code authentication
|
|
@@ -13,7 +15,10 @@
|
|
|
13
15
|
- ✅ **Автоматическое сохранение сессий** / Automatic session storage
|
|
14
16
|
- ✅ **Автовыбор транспорта** после QR-авторизации (переход на TCP)
|
|
15
17
|
- ✅ **Отправка и получение сообщений** / Send and receive messages
|
|
18
|
+
- ✅ **Загрузка медиа с диска:** `uploadPhoto`, `uploadVideo`, `uploadFile`, `uploadAudio` → `attachments` в `sendMessage` / `sendMessageChannel` / `reply`
|
|
16
19
|
- ✅ **Скачивание вложений по URL** (`baseUrl` из `attaches`) во временный файл — `downloadUrlToTempFile`, `message.downloadAttachment()`
|
|
20
|
+
- ✅ **Группы и каналы:** создание, инвайты, админы, участники, ссылки, mute, подписка — см. раздел API
|
|
21
|
+
- ✅ **Реакции, пины, настройки профиля и приватности, контакты / блокировка**
|
|
17
22
|
- ✅ **Редактирование и удаление сообщений** / Edit and delete messages
|
|
18
23
|
- ✅ **Event-driven архитектура** / Event-driven architecture
|
|
19
24
|
- ✅ **Обработка входящих уведомлений** / Handle incoming notifications
|
|
@@ -288,14 +293,14 @@ const message = await client.sendMessage({
|
|
|
288
293
|
|
|
289
294
|
##### `sendMessageChannel(options)`
|
|
290
295
|
|
|
291
|
-
Отправляет сообщение в канал без уведомления (notify: false).
|
|
296
|
+
Отправляет сообщение в канал без уведомления (notify: false). Поля **`text`**, **`replyTo`**, **`attachments`** — те же, что у `sendMessage` (вложения из `uploadPhoto` / `uploadVideo` / `uploadFile` / `uploadAudio`).
|
|
292
297
|
|
|
293
298
|
```javascript
|
|
294
299
|
const message = await client.sendMessageChannel({
|
|
295
300
|
chatId: 123,
|
|
296
301
|
text: 'Сообщение в канал',
|
|
297
|
-
replyTo: null,
|
|
298
|
-
attachments: []
|
|
302
|
+
replyTo: null,
|
|
303
|
+
attachments: [] // опционально: [attach] после upload*
|
|
299
304
|
});
|
|
300
305
|
```
|
|
301
306
|
|
|
@@ -307,10 +312,57 @@ const message = await client.sendMessageChannel({
|
|
|
307
312
|
await client.editMessage({
|
|
308
313
|
messageId: 456,
|
|
309
314
|
chatId: 123,
|
|
310
|
-
text: 'Исправленный текст'
|
|
315
|
+
text: 'Исправленный текст',
|
|
316
|
+
attachments: [] // опционально, после upload*
|
|
311
317
|
});
|
|
312
318
|
```
|
|
313
319
|
|
|
320
|
+
##### Пины, реакции
|
|
321
|
+
|
|
322
|
+
| Метод | Назначение |
|
|
323
|
+
|--------|------------|
|
|
324
|
+
| `pinMessage({ chatId, messageId, notifyPin })` | Закрепить сообщение |
|
|
325
|
+
| `setMessageReaction({ chatId, messageId, emoji })` | Эмодзи-реакция |
|
|
326
|
+
| `cancelMessageReaction({ chatId, messageId })` | Снять реакцию |
|
|
327
|
+
| `getMessageReactions({ chatId, messageId, count })` | Список реакций |
|
|
328
|
+
|
|
329
|
+
##### Чаты, каналы, группы
|
|
330
|
+
|
|
331
|
+
| Метод | Назначение |
|
|
332
|
+
|--------|------------|
|
|
333
|
+
| `getChatInfo(chatIds)` | Информация по id (массив или одно число) |
|
|
334
|
+
| `resolveLink(link)` | Разрешить URL / `join/…` (LINK_INFO) |
|
|
335
|
+
| `joinChatByLink(link)` | Вступить по ссылке |
|
|
336
|
+
| `setChatSubscription(chatId, subscribe)` | Подписка на канал |
|
|
337
|
+
| `createGroup({ title, userIds })` | Новая группа |
|
|
338
|
+
| `createChannel({ title })` | Новый канал |
|
|
339
|
+
| `muteChat(chatId, mute)` | Уведомления чата (не беспокоить) |
|
|
340
|
+
| `getChatMembers({ chatId, marker, count, type })` | Участники (count ≤ 500) |
|
|
341
|
+
| `inviteToChat({ chatId, userIds, showHistory })` | Пригласить |
|
|
342
|
+
| `removeFromChat({ chatId, userIds, cleanMsgPeriod })` | Исключить |
|
|
343
|
+
| `addChatAdmins({ chatId, userIds, permissions })` | Выдать админку (по умолчанию `permissions: 120`) |
|
|
344
|
+
| `removeChatAdmins({ chatId, userIds })` | Снять админку |
|
|
345
|
+
| `transferChatOwnership({ chatId, newOwnerId })` | Передать владение |
|
|
346
|
+
| `setGroupOptions({ chatId, options })` | Настройки группы (`ALL_CAN_PIN_MESSAGE`, …) |
|
|
347
|
+
| `resolveChannelByUsername(username)` | Канал по @username |
|
|
348
|
+
| `joinChannelByUsername(username)` | Вступить по @username |
|
|
349
|
+
| `resolveInviteHash(hash)` | Инвайт по хэшу без префикса `join/` |
|
|
350
|
+
|
|
351
|
+
##### Контакты и профиль
|
|
352
|
+
|
|
353
|
+
| Метод | Назначение |
|
|
354
|
+
|--------|------------|
|
|
355
|
+
| `getContacts(contactIds)` | Несколько контактов (массив id) |
|
|
356
|
+
| `addContact(userId)` | В контакты |
|
|
357
|
+
| `blockUser(userId)` | Заблокировать |
|
|
358
|
+
| `updateProfile({ firstName, lastName, description })` | Своё имя / описание |
|
|
359
|
+
| `setHiddenOnline(hidden)` | Скрыть «в сети» |
|
|
360
|
+
| `setFindableByPhone(mode)` | `'ALL'` \| `'CONTACTS'` или boolean |
|
|
361
|
+
| `setCallsPrivacyMode(mode)` | Кто может звонить |
|
|
362
|
+
| `setChatsInvitePrivacy(mode)` | Кто может приглашать в чаты |
|
|
363
|
+
|
|
364
|
+
Часть методов требует прав в чате; ответы сервера зависят от роли и типа чата.
|
|
365
|
+
|
|
314
366
|
##### `deleteMessage(options)`
|
|
315
367
|
|
|
316
368
|
Удаляет сообщение.
|
|
@@ -334,6 +386,36 @@ await client.forwardMessage({
|
|
|
334
386
|
});
|
|
335
387
|
```
|
|
336
388
|
|
|
389
|
+
##### Загрузка медиа для `attachments`
|
|
390
|
+
|
|
391
|
+
Все методы ниже возвращают объект(ы), которые передаются в **`attachments`** у `sendMessage`, **`sendMessageChannel`** и **`message.reply`**. Нужен **Node.js 18+** (`fetch`, `FormData`). Схема: опкод загрузки → `UPLOAD_ATTACH_PREP` (65) → HTTP POST на выданный URL. Для **видео** и **файлов** после POST клиент ждёт **`NOTIF_ATTACH` (opcode 136)**.
|
|
392
|
+
|
|
393
|
+
| Метод | Результат для `attachments` |
|
|
394
|
+
|--------|-----------------------------|
|
|
395
|
+
| `uploadPhoto(chatId, filePath)` | `{ _type: 'PHOTO', photoToken }` |
|
|
396
|
+
| `uploadVideo(chatId, filePath)` | `{ _type: 'VIDEO', videoId, token }` |
|
|
397
|
+
| `uploadFile(chatId, filePath, options?)` | `{ _type: 'FILE', fileId }` — документы, архивы; `options`: `{ filename, mimeType }` |
|
|
398
|
+
| `uploadAudio(chatId, filePath)` | то же, что `uploadFile` с MIME для `.mp3`, `.ogg`, `.m4a`, `.wav`, … |
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
const photo = await client.uploadPhoto(chatId, './a.png');
|
|
402
|
+
const video = await client.uploadVideo(chatId, './b.mp4');
|
|
403
|
+
const file = await client.uploadFile(chatId, './doc.pdf');
|
|
404
|
+
const audio = await client.uploadAudio(chatId, './track.mp3');
|
|
405
|
+
|
|
406
|
+
await client.sendMessage({
|
|
407
|
+
chatId,
|
|
408
|
+
text: 'Набор вложений',
|
|
409
|
+
attachments: [photo, video]
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await client.sendMessageChannel({
|
|
413
|
+
chatId,
|
|
414
|
+
text: 'В канал с файлом',
|
|
415
|
+
attachments: [file]
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
337
419
|
##### `sendChatAction(chatId, action)`
|
|
338
420
|
|
|
339
421
|
Отправляет действие в чате (печатает, выбирает стикер и т.д.).
|
|
@@ -679,6 +761,7 @@ webmaxsocket/
|
|
|
679
761
|
├── example-sms.js # SMS авторизация
|
|
680
762
|
├── example-ios.js # IOS/ANDROID Socket
|
|
681
763
|
├── package.json
|
|
764
|
+
├── api.package.md # Справочник API (все методы)
|
|
682
765
|
└── README.md
|
|
683
766
|
```
|
|
684
767
|
|
|
@@ -742,6 +825,8 @@ DEBUG=1 node example.js
|
|
|
742
825
|
|
|
743
826
|
6. **`cid` при отправке сообщений (TCP/Socket):** сервер проверяет **signed int32**. Не передавайте `Date.now()` (миллисекунды ~1e12) — будет «Ошибка валидации». Либо не указывайте `cid` (клиент подставит свой), либо передайте целое в диапазоне **−2³¹ … 2³¹−1**.
|
|
744
827
|
|
|
828
|
+
7. **TCP и keep-alive (PING):** сервер периодически шлёт `PING`. На WebSocket клиент отвечает `sendPong`; на **TCP** раньше ответ не отправлялся — соединение могло обрываться через несколько минут, после чего процесс Node **завершался** (нечем держать event loop). Сейчас на TCP автоматически шлётся тот же ответ, что и у WebSocket (`PING` с пустым payload).
|
|
829
|
+
|
|
745
830
|
## 🔗 Ссылки / Links
|
|
746
831
|
|
|
747
832
|
- [GitHub Repository](https://github.com/Tellarion/webmaxsocket)
|
package/api.package.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# WebMaxSocket — справочник API
|
|
2
|
+
|
|
3
|
+
Краткий перечень возможностей для работы с библиотекой. Подробности и примеры — в `README.md`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Экспорт пакета (`require('webmaxsocket')`)
|
|
8
|
+
|
|
9
|
+
| Символ | Описание |
|
|
10
|
+
|--------|----------|
|
|
11
|
+
| `WebMaxClient` | Основной клиент |
|
|
12
|
+
| `MaxSocketTransport` | Низкоуровневый TCP-транспорт |
|
|
13
|
+
| `User`, `Message`, `ChatAction` | Сущности |
|
|
14
|
+
| `ChatActions`, `EventTypes`, `MessageTypes` | Константы |
|
|
15
|
+
| `Opcode`, `getOpcodeName` | Опкоды протокола |
|
|
16
|
+
| `UserAgentPayload` | User-Agent для handshake |
|
|
17
|
+
| `downloadUrlToTempFile`, `extFromContentType`, `extFromAttachType` | Скачивание медиа по URL |
|
|
18
|
+
| `resolveIncomingLogMode`, `printIncomingLog` | Режим лога входящих |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## `WebMaxClient`
|
|
23
|
+
|
|
24
|
+
Клиент наследует `EventEmitter`: `on`, `once`, `emit`, `off` (в т.ч. событие `connected`, `raw_message`).
|
|
25
|
+
|
|
26
|
+
### Свойства (полезные)
|
|
27
|
+
|
|
28
|
+
| Свойство | Описание |
|
|
29
|
+
|----------|----------|
|
|
30
|
+
| `me` | Профиль после авторизации |
|
|
31
|
+
| `session` | Менеджер сессий |
|
|
32
|
+
| `isConnected`, `isAuthorized` | Состояние |
|
|
33
|
+
| `userAgent` | Текущий User-Agent |
|
|
34
|
+
| `deviceId` | ID устройства |
|
|
35
|
+
| `incomingLogMode` | `'off'` \| `'messages'` \| `'verbose'` |
|
|
36
|
+
|
|
37
|
+
### Запуск и соединение
|
|
38
|
+
|
|
39
|
+
| Метод | Описание |
|
|
40
|
+
|-------|----------|
|
|
41
|
+
| `start()` | Подключение, авторизация, обработчики `onStart` |
|
|
42
|
+
| `connect()` | Только соединение (низкоуровнево) |
|
|
43
|
+
| `connectWithSession()` | Сессия + синхронизация |
|
|
44
|
+
| `handshake()` | Handshake (WebSocket-путь) |
|
|
45
|
+
| `sync()` | LOGIN по токену |
|
|
46
|
+
| `fetchMyProfile()` | Загрузка `me` |
|
|
47
|
+
| `stop()` | Закрыть транспорт |
|
|
48
|
+
| `logout()` | Остановка + удаление сессии |
|
|
49
|
+
|
|
50
|
+
### Авторизация
|
|
51
|
+
|
|
52
|
+
| Метод | Описание |
|
|
53
|
+
|-------|----------|
|
|
54
|
+
| `authorize(phone?)` | Сценарий авторизации по умолчанию |
|
|
55
|
+
| `authorizeByQR()` | QR (WEB) |
|
|
56
|
+
| `authorizeBySMS(phone)` | SMS; возвращает `{ sendCode }` |
|
|
57
|
+
| `requestQR()` | Запрос QR |
|
|
58
|
+
| `checkQRStatus(trackId)` | Статус QR |
|
|
59
|
+
| `loginByQR(trackId)` | Завершение по QR |
|
|
60
|
+
| `pollQRStatus(...)` | Ожидание сканирования QR |
|
|
61
|
+
| `showLinkDeviceQR(options?)` | QR «подключить устройство» после входа |
|
|
62
|
+
|
|
63
|
+
### Сообщения
|
|
64
|
+
|
|
65
|
+
| Метод | Описание |
|
|
66
|
+
|-------|----------|
|
|
67
|
+
| `sendMessage({ chatId, text, cid?, replyTo?, attachments? })` | С уведомлением |
|
|
68
|
+
| `sendMessageChannel({ ... })` | Канал, `notify: false` |
|
|
69
|
+
| `editMessage({ chatId, messageId, text, attachments? })` | Редактирование |
|
|
70
|
+
| `deleteMessage({ chatId, messageId, forMe? })` | Удаление |
|
|
71
|
+
| `uploadPhoto(chatId, filePath)` | Вложение фото → `attachments` |
|
|
72
|
+
| `uploadVideo(chatId, filePath)` | Видео |
|
|
73
|
+
| `uploadFile(chatId, filePath, options?)` | Файл |
|
|
74
|
+
| `uploadAudio(chatId, filePath)` | Аудио как файл |
|
|
75
|
+
|
|
76
|
+
### Пины и реакции
|
|
77
|
+
|
|
78
|
+
| Метод | Описание |
|
|
79
|
+
|-------|----------|
|
|
80
|
+
| `pinMessage({ chatId, messageId, notifyPin? })` | Закрепить |
|
|
81
|
+
| `setMessageReaction({ chatId, messageId, emoji })` | Реакция |
|
|
82
|
+
| `cancelMessageReaction({ chatId, messageId })` | Снять реакцию |
|
|
83
|
+
| `getMessageReactions({ chatId, messageId, count? })` | Список реакций |
|
|
84
|
+
|
|
85
|
+
### Чаты, каналы, группы
|
|
86
|
+
|
|
87
|
+
| Метод | Описание |
|
|
88
|
+
|-------|----------|
|
|
89
|
+
| `getChats(marker?)` | Список чатов |
|
|
90
|
+
| `getHistory(chatId, from?, backward?, forward?)` | История |
|
|
91
|
+
| `getChatInfo(chatIds)` | Инфо по id |
|
|
92
|
+
| `resolveLink(link)` | LINK_INFO |
|
|
93
|
+
| `joinChatByLink(link)` | Вступить по ссылке |
|
|
94
|
+
| `setChatSubscription(chatId, subscribe)` | Подписка на канал |
|
|
95
|
+
| `createGroup({ title, userIds })` | Новая группа |
|
|
96
|
+
| `createChannel({ title })` | Новый канал |
|
|
97
|
+
| `muteChat(chatId, mute?)` | Не беспокоить для чата |
|
|
98
|
+
| `getChatMembers({ chatId, marker?, count?, type? })` | Участники |
|
|
99
|
+
| `inviteToChat({ chatId, userIds, showHistory? })` | Пригласить |
|
|
100
|
+
| `removeFromChat({ chatId, userIds, cleanMsgPeriod? })` | Исключить |
|
|
101
|
+
| `addChatAdmins({ chatId, userIds, permissions? })` | Админы |
|
|
102
|
+
| `removeChatAdmins({ chatId, userIds })` | Снять админов |
|
|
103
|
+
| `transferChatOwnership({ chatId, newOwnerId })` | Смена владельца |
|
|
104
|
+
| `setGroupOptions({ chatId, options })` | Настройки группы |
|
|
105
|
+
| `resolveChannelByUsername(username)` | Канал по @ |
|
|
106
|
+
| `joinChannelByUsername(username)` | Вступить по @ |
|
|
107
|
+
| `resolveInviteHash(hash)` | Инвайт `join/...` |
|
|
108
|
+
|
|
109
|
+
### Контакты и профиль
|
|
110
|
+
|
|
111
|
+
| Метод | Описание |
|
|
112
|
+
|-------|----------|
|
|
113
|
+
| `getUser(userId)` | Один контакт → `User` |
|
|
114
|
+
| `getContacts(contactIds)` | Несколько контактов (сырой ответ) |
|
|
115
|
+
| `addContact(userId)` | В контакты |
|
|
116
|
+
| `blockUser(userId)` | Блокировка |
|
|
117
|
+
| `updateProfile({ firstName?, lastName?, description? })` | Профиль |
|
|
118
|
+
| `setHiddenOnline(hidden)` | Скрыть онлайн |
|
|
119
|
+
| `setFindableByPhone(mode)` | Поиск по телефону |
|
|
120
|
+
| `setCallsPrivacyMode(mode)` | Звонки |
|
|
121
|
+
| `setChatsInvitePrivacy(mode)` | Приглашения в чаты |
|
|
122
|
+
|
|
123
|
+
### Обработчики событий
|
|
124
|
+
|
|
125
|
+
| Метод | Событие |
|
|
126
|
+
|-------|---------|
|
|
127
|
+
| `onStart(handler)` | Успешный старт |
|
|
128
|
+
| `onMessage(handler)` | Новое сообщение |
|
|
129
|
+
| `onMessageRemoved(handler)` | Удалено сообщение |
|
|
130
|
+
| `onChatAction(handler)` | Действие в чате |
|
|
131
|
+
| `onError(handler)` | Ошибки |
|
|
132
|
+
|
|
133
|
+
### Прочее
|
|
134
|
+
|
|
135
|
+
| Метод | Описание |
|
|
136
|
+
|-------|----------|
|
|
137
|
+
| `logIncoming(label, payload)` | Ручной лог `[incoming:…]` |
|
|
138
|
+
| `sendAndWait(opcode, payload, cmd?, timeout?)` | Низкоуровневый RPC |
|
|
139
|
+
| `triggerHandlers(eventType, data?)` | Внутренний вызов обработчиков (редко нужен снаружи) |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## `Message`
|
|
144
|
+
|
|
145
|
+
| Метод / свойство | Описание |
|
|
146
|
+
|------------------|----------|
|
|
147
|
+
| `id`, `cid`, `chatId`, `text`, `senderId`, `sender`, `attachments`, `rawData`, … | Поля |
|
|
148
|
+
| `fetchSender()` | Подгрузить отправителя |
|
|
149
|
+
| `getSenderName()` | Имя для лога |
|
|
150
|
+
| `reply({ text, cid?, attachments?, quote? })` | Ответ в чат |
|
|
151
|
+
| `edit({ text, … })` | Редактировать |
|
|
152
|
+
| `delete()` | Удалить |
|
|
153
|
+
| `forward(chatId)` | В `Message` вызывает `client.forwardMessage` — метод на клиенте может отсутствовать, проверьте версию |
|
|
154
|
+
| `downloadAttachment(index?, options?)` | Скачать вложение по `baseUrl` во временный файл |
|
|
155
|
+
| `toJSON()`, `toString()` | Сериализация |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## `ChatAction`
|
|
160
|
+
|
|
161
|
+
| Свойство | Описание |
|
|
162
|
+
|----------|----------|
|
|
163
|
+
| `type`, `chatId`, `userId`, `user`, `timestamp`, `rawData` | Данные действия |
|
|
164
|
+
| `toString()`, `toJSON()` | Представление |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## `User`
|
|
169
|
+
|
|
170
|
+
Основные поля: `id`, `firstname`, `lastname`, `phone`, `avatar`, `fullname` (getter) и др. — см. `lib/entities/User.js`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Константы
|
|
175
|
+
|
|
176
|
+
- **`EventTypes`**: `START`, `MESSAGE`, `MESSAGE_REMOVED`, `CHAT_ACTION`, `ERROR`, `DISCONNECT`
|
|
177
|
+
- **`ChatActions`**: `TYPING`, `STICKER`, `FILE`, `RECORDING_VOICE`, `RECORDING_VIDEO`
|
|
178
|
+
- **`MessageTypes`**: `TEXT`, `IMAGE`, `VIDEO`, `AUDIO`, `DOCUMENT`, `STICKER`
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Утилиты (не методы клиента)
|
|
183
|
+
|
|
184
|
+
| Функция | Описание |
|
|
185
|
+
|---------|----------|
|
|
186
|
+
| `downloadUrlToTempFile(url, { dir?, filename?, extFallback? })` | HTTP → временный файл |
|
|
187
|
+
| `extFromContentType`, `extFromAttachType` | Подбор расширения |
|
|
188
|
+
| `resolveIncomingLogMode(options)` | Режим `logIncoming` из опций конструктора |
|
|
189
|
+
| `printIncomingLog(label, payload)` | Печать JSON в консоль |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
*Файл для быстрой навигации; при расхождении с кодом приоритет у реализации в `lib/`.*
|
package/lib/client.js
CHANGED
|
@@ -154,6 +154,9 @@ class WebMaxClient extends EventEmitter {
|
|
|
154
154
|
this._wireIncomingLogListeners();
|
|
155
155
|
/** client id локальных исходящих сообщений (int32, часто ждут валидацию на сервере) */
|
|
156
156
|
this._clientSendCid = 1 + Math.floor(Math.random() * 0xfffff);
|
|
157
|
+
/** После HTTP POST видео/файла — ждём NOTIF_ATTACH */
|
|
158
|
+
this._uploadPendingVideo = new Map();
|
|
159
|
+
this._uploadPendingFile = new Map();
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
/**
|
|
@@ -929,8 +932,20 @@ class WebMaxClient extends EventEmitter {
|
|
|
929
932
|
break;
|
|
930
933
|
|
|
931
934
|
case Opcode.PING:
|
|
935
|
+
// Иначе сервер рвёт TCP через ~минуты; WebSocket здесь шлёт sendPong (тот же opcode PING + {})
|
|
936
|
+
if (
|
|
937
|
+
this._socketTransport &&
|
|
938
|
+
this._socketTransport.socket &&
|
|
939
|
+
!this._socketTransport.socket.destroyed
|
|
940
|
+
) {
|
|
941
|
+
this._socketTransport.sendOneWay(Opcode.PING, {});
|
|
942
|
+
}
|
|
932
943
|
break;
|
|
933
|
-
|
|
944
|
+
|
|
945
|
+
case Opcode.NOTIF_ATTACH:
|
|
946
|
+
this._handleNotifAttach(data.payload);
|
|
947
|
+
break;
|
|
948
|
+
|
|
934
949
|
default:
|
|
935
950
|
this.emit('raw_message', data);
|
|
936
951
|
}
|
|
@@ -998,7 +1013,11 @@ class WebMaxClient extends EventEmitter {
|
|
|
998
1013
|
case Opcode.PING:
|
|
999
1014
|
await this.sendPong();
|
|
1000
1015
|
break;
|
|
1001
|
-
|
|
1016
|
+
|
|
1017
|
+
case Opcode.NOTIF_ATTACH:
|
|
1018
|
+
this._handleNotifAttach(message.payload);
|
|
1019
|
+
break;
|
|
1020
|
+
|
|
1002
1021
|
default:
|
|
1003
1022
|
this.emit('raw_message', message);
|
|
1004
1023
|
}
|
|
@@ -1184,6 +1203,73 @@ class WebMaxClient extends EventEmitter {
|
|
|
1184
1203
|
return Number.isNaN(n) ? chatId : n;
|
|
1185
1204
|
}
|
|
1186
1205
|
|
|
1206
|
+
/**
|
|
1207
|
+
* NOTIF_ATTACH (136): готовность вложения после загрузки видео/файла.
|
|
1208
|
+
*/
|
|
1209
|
+
_handleNotifAttach(payload) {
|
|
1210
|
+
if (!payload || typeof payload !== 'object') return;
|
|
1211
|
+
const vid = payload.videoId;
|
|
1212
|
+
if (vid != null) {
|
|
1213
|
+
const k = String(vid);
|
|
1214
|
+
const fn = this._uploadPendingVideo.get(k);
|
|
1215
|
+
if (fn) {
|
|
1216
|
+
this._uploadPendingVideo.delete(k);
|
|
1217
|
+
fn();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const fid = payload.fileId;
|
|
1221
|
+
if (fid != null) {
|
|
1222
|
+
const k = String(fid);
|
|
1223
|
+
const fn = this._uploadPendingFile.get(k);
|
|
1224
|
+
if (fn) {
|
|
1225
|
+
this._uploadPendingFile.delete(k);
|
|
1226
|
+
fn();
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* @param {Map<string, function(): void>} map
|
|
1233
|
+
*/
|
|
1234
|
+
_waitUploadNotif(map, id, label, timeoutMs = 120000) {
|
|
1235
|
+
return new Promise((resolve, reject) => {
|
|
1236
|
+
const k = String(id);
|
|
1237
|
+
const t = setTimeout(() => {
|
|
1238
|
+
map.delete(k);
|
|
1239
|
+
reject(new Error(`Таймаут ожидания NOTIF_ATTACH (${label})`));
|
|
1240
|
+
}, timeoutMs);
|
|
1241
|
+
map.set(k, () => {
|
|
1242
|
+
clearTimeout(t);
|
|
1243
|
+
resolve();
|
|
1244
|
+
});
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async _postMultipartUpload(uploadUrl, buf, fname, mime) {
|
|
1249
|
+
const { Blob } = require('buffer');
|
|
1250
|
+
if (typeof fetch !== 'function') {
|
|
1251
|
+
throw new Error('upload: нужен Node.js 18+ с глобальным fetch');
|
|
1252
|
+
}
|
|
1253
|
+
const form = new FormData();
|
|
1254
|
+
form.append('file', new Blob([buf], { type: mime }), fname);
|
|
1255
|
+
const res = await fetch(uploadUrl, {
|
|
1256
|
+
method: 'POST',
|
|
1257
|
+
body: form,
|
|
1258
|
+
headers: {
|
|
1259
|
+
Accept: '*/*',
|
|
1260
|
+
'Accept-Language': 'ru-RU,ru;q=0.9',
|
|
1261
|
+
Origin: 'https://web.max.ru',
|
|
1262
|
+
Referer: 'https://web.max.ru/',
|
|
1263
|
+
'User-Agent': this.userAgent.headerUserAgent || 'Mozilla/5.0'
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
if (!res.ok) {
|
|
1267
|
+
const t = await res.text();
|
|
1268
|
+
throw new Error(`HTTP загрузка: ${res.status} ${t.slice(0, 300)}`);
|
|
1269
|
+
}
|
|
1270
|
+
return res;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1187
1273
|
/**
|
|
1188
1274
|
* Отправка сообщения (с уведомлением)
|
|
1189
1275
|
*/
|
|
@@ -1234,18 +1320,218 @@ class WebMaxClient extends EventEmitter {
|
|
|
1234
1320
|
return response.payload;
|
|
1235
1321
|
}
|
|
1236
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* Загрузка локального изображения на сервер Max; результат передать в `attachments` у sendMessage / reply.
|
|
1325
|
+
* Схема: PHOTO_UPLOAD → UPLOAD_ATTACH_PREP → HTTP POST на выданный URL. Нужен Node 18+ (fetch, FormData).
|
|
1326
|
+
*
|
|
1327
|
+
* @param {number|string|bigint} chatId
|
|
1328
|
+
* @param {string} filePath путь к файлу (.png, .jpg, …)
|
|
1329
|
+
* @returns {Promise<{ _type: 'PHOTO', photoToken: string }>}
|
|
1330
|
+
*/
|
|
1331
|
+
async uploadPhoto(chatId, filePath) {
|
|
1332
|
+
const fsp = require('fs/promises');
|
|
1333
|
+
const path = require('path');
|
|
1334
|
+
|
|
1335
|
+
const buf = await fsp.readFile(filePath);
|
|
1336
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1337
|
+
const mime =
|
|
1338
|
+
ext === '.png'
|
|
1339
|
+
? 'image/png'
|
|
1340
|
+
: ext === '.webp'
|
|
1341
|
+
? 'image/webp'
|
|
1342
|
+
: ext === '.gif'
|
|
1343
|
+
? 'image/gif'
|
|
1344
|
+
: 'image/jpeg';
|
|
1345
|
+
const fname = path.basename(filePath) || 'image.jpg';
|
|
1346
|
+
|
|
1347
|
+
const r1 = await this.sendAndWait(Opcode.PHOTO_UPLOAD, { count: 1 });
|
|
1348
|
+
const p1 = r1.payload;
|
|
1349
|
+
if (p1 && p1.error) {
|
|
1350
|
+
const e = new Error(
|
|
1351
|
+
typeof p1.error === 'string' ? p1.error : JSON.stringify(p1.error)
|
|
1352
|
+
);
|
|
1353
|
+
e.rawPayload = p1;
|
|
1354
|
+
throw e;
|
|
1355
|
+
}
|
|
1356
|
+
const uploadUrl = p1 && p1.url;
|
|
1357
|
+
if (!uploadUrl) {
|
|
1358
|
+
throw new Error('PHOTO_UPLOAD: нет url в ответе');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
await this.sendAndWait(Opcode.UPLOAD_ATTACH_PREP, {
|
|
1362
|
+
chatId: this._normalizeChatId(chatId),
|
|
1363
|
+
type: 'PHOTO'
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
const res = await this._postMultipartUpload(uploadUrl, buf, fname, mime);
|
|
1367
|
+
const obj = await res.json();
|
|
1368
|
+
const photos = obj.photos;
|
|
1369
|
+
let first;
|
|
1370
|
+
if (Array.isArray(photos)) {
|
|
1371
|
+
[first] = photos;
|
|
1372
|
+
} else if (photos && typeof photos === 'object') {
|
|
1373
|
+
first = Object.values(photos)[0];
|
|
1374
|
+
}
|
|
1375
|
+
const token = first && first.token;
|
|
1376
|
+
if (!token) {
|
|
1377
|
+
throw new Error(`PHOTO upload: неожиданный JSON: ${JSON.stringify(obj).slice(0, 400)}`);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
return {
|
|
1381
|
+
_type: 'PHOTO',
|
|
1382
|
+
photoToken: token
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Загрузка видео; результат для `attachments: [{ _type: 'VIDEO', videoId, token }]`.
|
|
1388
|
+
* После HTTP POST ждёт NOTIF_ATTACH (opcode 136).
|
|
1389
|
+
*/
|
|
1390
|
+
async uploadVideo(chatId, filePath) {
|
|
1391
|
+
const fsp = require('fs/promises');
|
|
1392
|
+
const path = require('path');
|
|
1393
|
+
|
|
1394
|
+
const buf = await fsp.readFile(filePath);
|
|
1395
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1396
|
+
const fname = path.basename(filePath) || 'video.mp4';
|
|
1397
|
+
const mime =
|
|
1398
|
+
ext === '.webm' ? 'video/webm' : ext === '.mov' ? 'video/quicktime' : 'video/mp4';
|
|
1399
|
+
|
|
1400
|
+
const r = await this.sendAndWait(Opcode.VIDEO_UPLOAD, { count: 1 });
|
|
1401
|
+
const p = r.payload;
|
|
1402
|
+
if (p && p.error) {
|
|
1403
|
+
const e = new Error(
|
|
1404
|
+
typeof p.error === 'string' ? p.error : JSON.stringify(p.error)
|
|
1405
|
+
);
|
|
1406
|
+
e.rawPayload = p;
|
|
1407
|
+
throw e;
|
|
1408
|
+
}
|
|
1409
|
+
const info = p && p.info && p.info[0];
|
|
1410
|
+
if (!info) {
|
|
1411
|
+
throw new Error('VIDEO_UPLOAD: нет info в ответе');
|
|
1412
|
+
}
|
|
1413
|
+
const { url: uploadUrl, videoId, token } = info;
|
|
1414
|
+
if (!uploadUrl || videoId == null || token == null) {
|
|
1415
|
+
throw new Error('VIDEO_UPLOAD: нет url, videoId или token');
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const waitReady = this._waitUploadNotif(this._uploadPendingVideo, videoId, 'VIDEO');
|
|
1419
|
+
|
|
1420
|
+
await this.sendAndWait(Opcode.UPLOAD_ATTACH_PREP, {
|
|
1421
|
+
chatId: this._normalizeChatId(chatId),
|
|
1422
|
+
type: 'VIDEO'
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
await this._postMultipartUpload(uploadUrl, buf, fname, mime);
|
|
1426
|
+
|
|
1427
|
+
await waitReady;
|
|
1428
|
+
|
|
1429
|
+
return { _type: 'VIDEO', videoId, token };
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Загрузка произвольного файла (документ, архив, **аудио** и т.д.) для `attachments: [{ _type: 'FILE', fileId }]`.
|
|
1434
|
+
* После HTTP POST ждёт NOTIF_ATTACH.
|
|
1435
|
+
*
|
|
1436
|
+
* @param {{ filename?: string, mimeType?: string }} [options]
|
|
1437
|
+
*/
|
|
1438
|
+
async uploadFile(chatId, filePath, options = {}) {
|
|
1439
|
+
const fsp = require('fs/promises');
|
|
1440
|
+
const path = require('path');
|
|
1441
|
+
|
|
1442
|
+
const buf = await fsp.readFile(filePath);
|
|
1443
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1444
|
+
const fname = options.filename || path.basename(filePath) || 'file.bin';
|
|
1445
|
+
const mime =
|
|
1446
|
+
options.mimeType ||
|
|
1447
|
+
this._mimeGuessForFile(ext);
|
|
1448
|
+
|
|
1449
|
+
const r = await this.sendAndWait(Opcode.FILE_UPLOAD, { count: 1 });
|
|
1450
|
+
const p = r.payload;
|
|
1451
|
+
if (p && p.error) {
|
|
1452
|
+
const e = new Error(
|
|
1453
|
+
typeof p.error === 'string' ? p.error : JSON.stringify(p.error)
|
|
1454
|
+
);
|
|
1455
|
+
e.rawPayload = p;
|
|
1456
|
+
throw e;
|
|
1457
|
+
}
|
|
1458
|
+
const info = p && p.info && p.info[0];
|
|
1459
|
+
if (!info) {
|
|
1460
|
+
throw new Error('FILE_UPLOAD: нет info в ответе');
|
|
1461
|
+
}
|
|
1462
|
+
const { url: uploadUrl, fileId } = info;
|
|
1463
|
+
if (!uploadUrl || fileId == null) {
|
|
1464
|
+
throw new Error('FILE_UPLOAD: нет url или fileId');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const waitReady = this._waitUploadNotif(this._uploadPendingFile, fileId, 'FILE');
|
|
1468
|
+
|
|
1469
|
+
await this.sendAndWait(Opcode.UPLOAD_ATTACH_PREP, {
|
|
1470
|
+
chatId: this._normalizeChatId(chatId),
|
|
1471
|
+
type: 'FILE'
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
await this._postMultipartUpload(uploadUrl, buf, fname, mime);
|
|
1475
|
+
|
|
1476
|
+
await waitReady;
|
|
1477
|
+
|
|
1478
|
+
return { _type: 'FILE', fileId };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Загрузка аудио как файла (удобно для .mp3, .ogg, .m4a, .wav).
|
|
1483
|
+
* Внутри вызывает uploadFile() с подходящим MIME.
|
|
1484
|
+
*/
|
|
1485
|
+
async uploadAudio(chatId, filePath) {
|
|
1486
|
+
const path = require('path');
|
|
1487
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1488
|
+
const mime =
|
|
1489
|
+
ext === '.mp3'
|
|
1490
|
+
? 'audio/mpeg'
|
|
1491
|
+
: ext === '.ogg' || ext === '.oga'
|
|
1492
|
+
? 'audio/ogg'
|
|
1493
|
+
: ext === '.m4a' || ext === '.aac'
|
|
1494
|
+
? 'audio/mp4'
|
|
1495
|
+
: ext === '.wav'
|
|
1496
|
+
? 'audio/wav'
|
|
1497
|
+
: ext === '.flac'
|
|
1498
|
+
? 'audio/flac'
|
|
1499
|
+
: 'audio/mpeg';
|
|
1500
|
+
return this.uploadFile(chatId, filePath, { mimeType: mime });
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
_mimeGuessForFile(ext) {
|
|
1504
|
+
const m = {
|
|
1505
|
+
'.png': 'image/png',
|
|
1506
|
+
'.jpg': 'image/jpeg',
|
|
1507
|
+
'.jpeg': 'image/jpeg',
|
|
1508
|
+
'.gif': 'image/gif',
|
|
1509
|
+
'.webp': 'image/webp',
|
|
1510
|
+
'.pdf': 'application/pdf',
|
|
1511
|
+
'.zip': 'application/zip',
|
|
1512
|
+
'.mp3': 'audio/mpeg',
|
|
1513
|
+
'.ogg': 'audio/ogg',
|
|
1514
|
+
'.m4a': 'audio/mp4',
|
|
1515
|
+
'.wav': 'audio/wav',
|
|
1516
|
+
'.flac': 'audio/flac',
|
|
1517
|
+
'.mp4': 'video/mp4',
|
|
1518
|
+
'.webm': 'video/webm'
|
|
1519
|
+
};
|
|
1520
|
+
return m[ext] || 'application/octet-stream';
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1237
1523
|
/**
|
|
1238
1524
|
* Редактирование сообщения
|
|
1239
1525
|
*/
|
|
1240
1526
|
async editMessage(options) {
|
|
1241
|
-
const { messageId, chatId, text } = options;
|
|
1527
|
+
const { messageId, chatId, text, attachments } = options;
|
|
1242
1528
|
|
|
1243
1529
|
const payload = {
|
|
1244
1530
|
chatId: chatId,
|
|
1245
1531
|
messageId: messageId,
|
|
1246
1532
|
text: text || '',
|
|
1247
1533
|
elements: [],
|
|
1248
|
-
attaches: []
|
|
1534
|
+
attaches: Array.isArray(attachments) && attachments.length ? attachments : []
|
|
1249
1535
|
};
|
|
1250
1536
|
|
|
1251
1537
|
const response = await this.sendAndWait(Opcode.MSG_EDIT, payload);
|
|
@@ -1346,6 +1632,360 @@ class WebMaxClient extends EventEmitter {
|
|
|
1346
1632
|
return messages.map(msg => new Message(msg, this));
|
|
1347
1633
|
}
|
|
1348
1634
|
|
|
1635
|
+
/**
|
|
1636
|
+
* Закрепить сообщение в чате (CHAT_UPDATE).
|
|
1637
|
+
*/
|
|
1638
|
+
async pinMessage({ chatId, messageId, notifyPin = false }) {
|
|
1639
|
+
return await this.sendAndWait(Opcode.CHAT_UPDATE, {
|
|
1640
|
+
chatId: this._normalizeChatId(chatId),
|
|
1641
|
+
messageId: String(messageId),
|
|
1642
|
+
notifyPin: !!notifyPin
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* Поставить реакцию-эмодзи на сообщение.
|
|
1648
|
+
*/
|
|
1649
|
+
async setMessageReaction({ chatId, messageId, emoji }) {
|
|
1650
|
+
return await this.sendAndWait(Opcode.MSG_REACTION, {
|
|
1651
|
+
chatId: this._normalizeChatId(chatId),
|
|
1652
|
+
messageId: String(messageId),
|
|
1653
|
+
reaction: {
|
|
1654
|
+
reactionType: 'EMOJI',
|
|
1655
|
+
id: String(emoji)
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Снять свою реакцию с сообщения.
|
|
1662
|
+
*/
|
|
1663
|
+
async cancelMessageReaction({ chatId, messageId }) {
|
|
1664
|
+
return await this.sendAndWait(Opcode.MSG_CANCEL_REACTION, {
|
|
1665
|
+
chatId: this._normalizeChatId(chatId),
|
|
1666
|
+
messageId: String(messageId)
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Список реакций на сообщение.
|
|
1672
|
+
*/
|
|
1673
|
+
async getMessageReactions({ chatId, messageId, count = 100 }) {
|
|
1674
|
+
return await this.sendAndWait(Opcode.MSG_GET_REACTIONS, {
|
|
1675
|
+
chatId: this._normalizeChatId(chatId),
|
|
1676
|
+
messageId: String(messageId),
|
|
1677
|
+
count
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Информация о чатах по id (opcode 48).
|
|
1683
|
+
*/
|
|
1684
|
+
async getChatInfo(chatIds) {
|
|
1685
|
+
const ids = Array.isArray(chatIds) ? chatIds : [chatIds];
|
|
1686
|
+
const response = await this.sendAndWait(Opcode.CHAT_INFO, { chatIds: ids });
|
|
1687
|
+
return response.payload;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Разрешить ссылку: канал, инвайт join/…, URL max.ru (LINK_INFO).
|
|
1692
|
+
*/
|
|
1693
|
+
async resolveLink(link) {
|
|
1694
|
+
const response = await this.sendAndWait(Opcode.LINK_INFO, { link: String(link) });
|
|
1695
|
+
return response.payload;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Вступить по ссылке (канал, группа и т.д.).
|
|
1700
|
+
*/
|
|
1701
|
+
async joinChatByLink(link) {
|
|
1702
|
+
const response = await this.sendAndWait(Opcode.CHAT_JOIN, { link: String(link) });
|
|
1703
|
+
return response.payload;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* Подписка / отписка на канал.
|
|
1708
|
+
*/
|
|
1709
|
+
async setChatSubscription(chatId, subscribe) {
|
|
1710
|
+
return await this.sendAndWait(Opcode.CHAT_SUBSCRIBE, {
|
|
1711
|
+
chatId: this._normalizeChatId(chatId),
|
|
1712
|
+
subscribe: !!subscribe
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Создать групповой чат (CONTROL в MSG_SEND).
|
|
1718
|
+
*/
|
|
1719
|
+
async createGroup({ title, userIds }) {
|
|
1720
|
+
const cid = this._nextClientMessageId();
|
|
1721
|
+
return await this.sendAndWait(Opcode.MSG_SEND, {
|
|
1722
|
+
message: {
|
|
1723
|
+
text: '',
|
|
1724
|
+
cid,
|
|
1725
|
+
elements: [],
|
|
1726
|
+
attaches: [
|
|
1727
|
+
{
|
|
1728
|
+
_type: 'CONTROL',
|
|
1729
|
+
event: 'new',
|
|
1730
|
+
chatType: 'CHAT',
|
|
1731
|
+
title,
|
|
1732
|
+
userIds
|
|
1733
|
+
}
|
|
1734
|
+
]
|
|
1735
|
+
},
|
|
1736
|
+
notify: true
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* Создать канал (CONTROL в MSG_SEND).
|
|
1742
|
+
*/
|
|
1743
|
+
async createChannel({ title }) {
|
|
1744
|
+
const cid = this._nextClientMessageId();
|
|
1745
|
+
return await this.sendAndWait(Opcode.MSG_SEND, {
|
|
1746
|
+
message: {
|
|
1747
|
+
text: '',
|
|
1748
|
+
cid,
|
|
1749
|
+
elements: [],
|
|
1750
|
+
attaches: [
|
|
1751
|
+
{
|
|
1752
|
+
_type: 'CONTROL',
|
|
1753
|
+
event: 'new',
|
|
1754
|
+
title,
|
|
1755
|
+
chatType: 'CHANNEL'
|
|
1756
|
+
}
|
|
1757
|
+
]
|
|
1758
|
+
},
|
|
1759
|
+
notify: true
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Отключить уведомления в чате (CONFIG), как «не беспокоить» для чата.
|
|
1765
|
+
*/
|
|
1766
|
+
async muteChat(chatId, mute = true) {
|
|
1767
|
+
const id = String(this._normalizeChatId(chatId));
|
|
1768
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
1769
|
+
settings: {
|
|
1770
|
+
chats: {
|
|
1771
|
+
[id]: {
|
|
1772
|
+
dontDisturbUntil: mute ? -1 : 0
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Участники чата (не более 500 за запрос).
|
|
1781
|
+
*/
|
|
1782
|
+
async getChatMembers({ chatId, marker = 0, count = 500, type = 'MEMBER' }) {
|
|
1783
|
+
if (count > 500) {
|
|
1784
|
+
throw new Error('getChatMembers: count не больше 500');
|
|
1785
|
+
}
|
|
1786
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS, {
|
|
1787
|
+
type,
|
|
1788
|
+
marker,
|
|
1789
|
+
chatId: this._normalizeChatId(chatId),
|
|
1790
|
+
count
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Пригласить пользователей в чат.
|
|
1796
|
+
*/
|
|
1797
|
+
async inviteToChat({ chatId, userIds, showHistory = true }) {
|
|
1798
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
1799
|
+
chatId: this._normalizeChatId(chatId),
|
|
1800
|
+
userIds,
|
|
1801
|
+
showHistory,
|
|
1802
|
+
operation: 'add'
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* Исключить пользователей из чата.
|
|
1808
|
+
*/
|
|
1809
|
+
async removeFromChat({ chatId, userIds, cleanMsgPeriod = 0 }) {
|
|
1810
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
1811
|
+
chatId: this._normalizeChatId(chatId),
|
|
1812
|
+
userIds,
|
|
1813
|
+
operation: 'remove',
|
|
1814
|
+
cleanMsgPeriod
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Назначить администраторов. `permissions` — битовая маска прав (по умолчанию 120).
|
|
1820
|
+
*/
|
|
1821
|
+
async addChatAdmins({ chatId, userIds, permissions = 120 }) {
|
|
1822
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
1823
|
+
chatId: this._normalizeChatId(chatId),
|
|
1824
|
+
userIds,
|
|
1825
|
+
type: 'ADMIN',
|
|
1826
|
+
operation: 'add',
|
|
1827
|
+
permissions
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Снять права администратора.
|
|
1833
|
+
*/
|
|
1834
|
+
async removeChatAdmins({ chatId, userIds }) {
|
|
1835
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
1836
|
+
chatId: this._normalizeChatId(chatId),
|
|
1837
|
+
userIds,
|
|
1838
|
+
type: 'ADMIN',
|
|
1839
|
+
operation: 'remove'
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Передать владение группой.
|
|
1845
|
+
*/
|
|
1846
|
+
async transferChatOwnership({ chatId, newOwnerId }) {
|
|
1847
|
+
return await this.sendAndWait(Opcode.CHAT_UPDATE, {
|
|
1848
|
+
chatId: this._normalizeChatId(chatId),
|
|
1849
|
+
changeOwnerId: newOwnerId
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Настройки группы: например ONLY_OWNER_CAN_CHANGE_ICON_TITLE, ALL_CAN_PIN_MESSAGE, ONLY_ADMIN_CAN_ADD_MEMBER.
|
|
1855
|
+
*/
|
|
1856
|
+
async setGroupOptions({ chatId, options }) {
|
|
1857
|
+
return await this.sendAndWait(Opcode.CHAT_UPDATE, {
|
|
1858
|
+
chatId: this._normalizeChatId(chatId),
|
|
1859
|
+
options
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* Несколько контактов по id (сырой ответ CONTACT_INFO).
|
|
1865
|
+
*/
|
|
1866
|
+
async getContacts(contactIds) {
|
|
1867
|
+
const ids = Array.isArray(contactIds) ? contactIds : [contactIds];
|
|
1868
|
+
const response = await this.sendAndWait(Opcode.CONTACT_INFO, { contactIds: ids });
|
|
1869
|
+
return response.payload;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Добавить пользователя в контакты.
|
|
1874
|
+
*/
|
|
1875
|
+
async addContact(userId) {
|
|
1876
|
+
return await this.sendAndWait(Opcode.CONTACT_UPDATE, {
|
|
1877
|
+
contactId: userId,
|
|
1878
|
+
action: 'ADD'
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* Заблокировать пользователя.
|
|
1884
|
+
*/
|
|
1885
|
+
async blockUser(userId) {
|
|
1886
|
+
return await this.sendAndWait(Opcode.CONTACT_UPDATE, {
|
|
1887
|
+
contactId: userId,
|
|
1888
|
+
action: 'BLOCK'
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Изменить своё имя / описание (PROFILE).
|
|
1894
|
+
*/
|
|
1895
|
+
async updateProfile({ firstName, lastName, description } = {}) {
|
|
1896
|
+
const payload = {};
|
|
1897
|
+
if (firstName !== undefined) payload.firstName = firstName;
|
|
1898
|
+
if (lastName !== undefined) payload.lastName = lastName;
|
|
1899
|
+
if (description !== undefined) payload.description = description;
|
|
1900
|
+
return await this.sendAndWait(Opcode.PROFILE, payload);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Скрыть статус «в сети» для других.
|
|
1905
|
+
*/
|
|
1906
|
+
async setHiddenOnline(hidden) {
|
|
1907
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
1908
|
+
settings: {
|
|
1909
|
+
user: { HIDDEN: !!hidden }
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Кто может найти вас по телефону: 'ALL' | 'CONTACTS' или true/false (как ALL/CONTACTS).
|
|
1916
|
+
*/
|
|
1917
|
+
async setFindableByPhone(mode) {
|
|
1918
|
+
const v =
|
|
1919
|
+
mode === true || mode === 'ALL'
|
|
1920
|
+
? 'ALL'
|
|
1921
|
+
: mode === false || mode === 'CONTACTS'
|
|
1922
|
+
? 'CONTACTS'
|
|
1923
|
+
: String(mode);
|
|
1924
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
1925
|
+
settings: {
|
|
1926
|
+
user: { SEARCH_BY_PHONE: v }
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Кто может звонить: 'ALL' | 'CONTACTS'.
|
|
1933
|
+
*/
|
|
1934
|
+
async setCallsPrivacyMode(mode) {
|
|
1935
|
+
const v =
|
|
1936
|
+
mode === true || mode === 'ALL'
|
|
1937
|
+
? 'ALL'
|
|
1938
|
+
: mode === false || mode === 'CONTACTS'
|
|
1939
|
+
? 'CONTACTS'
|
|
1940
|
+
: String(mode);
|
|
1941
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
1942
|
+
settings: {
|
|
1943
|
+
user: { INCOMING_CALL: v }
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Кто может приглашать вас в чаты: 'ALL' | 'CONTACTS'.
|
|
1950
|
+
*/
|
|
1951
|
+
async setChatsInvitePrivacy(mode) {
|
|
1952
|
+
const v =
|
|
1953
|
+
mode === true || mode === 'ALL'
|
|
1954
|
+
? 'ALL'
|
|
1955
|
+
: mode === false || mode === 'CONTACTS'
|
|
1956
|
+
? 'CONTACTS'
|
|
1957
|
+
: String(mode);
|
|
1958
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
1959
|
+
settings: {
|
|
1960
|
+
user: { CHATS_INVITE: v }
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Удобно: канал по @username (resolveLink на https://max.ru/username).
|
|
1967
|
+
*/
|
|
1968
|
+
async resolveChannelByUsername(username) {
|
|
1969
|
+
const u = String(username).replace(/^@/, '');
|
|
1970
|
+
return this.resolveLink(`https://max.ru/${u}`);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Вступить в канал по @username.
|
|
1975
|
+
*/
|
|
1976
|
+
async joinChannelByUsername(username) {
|
|
1977
|
+
const u = String(username).replace(/^@/, '');
|
|
1978
|
+
return this.joinChatByLink(`https://max.ru/${u}`);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Инвайт по хэшу из ссылки join/XXXX.
|
|
1983
|
+
*/
|
|
1984
|
+
async resolveInviteHash(hash) {
|
|
1985
|
+
const h = String(hash).replace(/^join\//, '');
|
|
1986
|
+
return this.resolveLink(`join/${h}`);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1349
1989
|
/**
|
|
1350
1990
|
* Выполнение зарегистрированных обработчиков
|
|
1351
1991
|
*/
|
package/lib/opcodes.js
CHANGED
|
@@ -27,8 +27,11 @@ const Opcode = {
|
|
|
27
27
|
CHAT_LEAVE: 58,
|
|
28
28
|
CHAT_MEMBERS: 59,
|
|
29
29
|
MSG_SEND: 64,
|
|
30
|
+
UPLOAD_ATTACH_PREP: 65,
|
|
30
31
|
MSG_DELETE: 66,
|
|
31
32
|
MSG_EDIT: 67,
|
|
33
|
+
/** Подписка / отписка на канал (subscribe: true|false) */
|
|
34
|
+
CHAT_SUBSCRIBE: 75,
|
|
32
35
|
CHAT_MEMBERS_UPDATE: 77,
|
|
33
36
|
PHOTO_UPLOAD: 80,
|
|
34
37
|
VIDEO_UPLOAD: 82,
|
package/lib/socketTransport.js
CHANGED
|
@@ -156,6 +156,16 @@ class MaxSocketTransport {
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Однонаправленная отправка без ожидания ответа (ответ на серверный PING — как WebSocket sendPong).
|
|
161
|
+
*/
|
|
162
|
+
sendOneWay(opcode, payload = {}, cmd = 0) {
|
|
163
|
+
if (!this.socket || this.socket.destroyed) return;
|
|
164
|
+
const msg = this._makeMessage(opcode, payload, cmd);
|
|
165
|
+
const packet = packPacket(msg.ver, msg.cmd, msg.seq, msg.opcode, msg.payload);
|
|
166
|
+
this.socket.write(packet);
|
|
167
|
+
}
|
|
168
|
+
|
|
159
169
|
_startRecvLoop() {
|
|
160
170
|
const readNext = async () => {
|
|
161
171
|
if (!this.socket || this.socket.destroyed) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webmaxsocket",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Node.js client for Max Messenger with QR code and token authentication",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"example-token.js",
|
|
59
59
|
"example-sms.js",
|
|
60
60
|
"example-ios.js",
|
|
61
|
-
"README.md"
|
|
61
|
+
"README.md",
|
|
62
|
+
"api.package.md"
|
|
62
63
|
]
|
|
63
64
|
}
|
|
64
65
|
|