webmaxsocket 1.1.3 → 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 +87 -4
- package/api.package.md +193 -0
- package/lib/client.js +635 -3
- package/lib/opcodes.js +3 -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
|
|
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
|
/**
|
|
@@ -939,6 +942,10 @@ class WebMaxClient extends EventEmitter {
|
|
|
939
942
|
}
|
|
940
943
|
break;
|
|
941
944
|
|
|
945
|
+
case Opcode.NOTIF_ATTACH:
|
|
946
|
+
this._handleNotifAttach(data.payload);
|
|
947
|
+
break;
|
|
948
|
+
|
|
942
949
|
default:
|
|
943
950
|
this.emit('raw_message', data);
|
|
944
951
|
}
|
|
@@ -1006,7 +1013,11 @@ class WebMaxClient extends EventEmitter {
|
|
|
1006
1013
|
case Opcode.PING:
|
|
1007
1014
|
await this.sendPong();
|
|
1008
1015
|
break;
|
|
1009
|
-
|
|
1016
|
+
|
|
1017
|
+
case Opcode.NOTIF_ATTACH:
|
|
1018
|
+
this._handleNotifAttach(message.payload);
|
|
1019
|
+
break;
|
|
1020
|
+
|
|
1010
1021
|
default:
|
|
1011
1022
|
this.emit('raw_message', message);
|
|
1012
1023
|
}
|
|
@@ -1192,6 +1203,73 @@ class WebMaxClient extends EventEmitter {
|
|
|
1192
1203
|
return Number.isNaN(n) ? chatId : n;
|
|
1193
1204
|
}
|
|
1194
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
|
+
|
|
1195
1273
|
/**
|
|
1196
1274
|
* Отправка сообщения (с уведомлением)
|
|
1197
1275
|
*/
|
|
@@ -1242,18 +1320,218 @@ class WebMaxClient extends EventEmitter {
|
|
|
1242
1320
|
return response.payload;
|
|
1243
1321
|
}
|
|
1244
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
|
+
|
|
1245
1523
|
/**
|
|
1246
1524
|
* Редактирование сообщения
|
|
1247
1525
|
*/
|
|
1248
1526
|
async editMessage(options) {
|
|
1249
|
-
const { messageId, chatId, text } = options;
|
|
1527
|
+
const { messageId, chatId, text, attachments } = options;
|
|
1250
1528
|
|
|
1251
1529
|
const payload = {
|
|
1252
1530
|
chatId: chatId,
|
|
1253
1531
|
messageId: messageId,
|
|
1254
1532
|
text: text || '',
|
|
1255
1533
|
elements: [],
|
|
1256
|
-
attaches: []
|
|
1534
|
+
attaches: Array.isArray(attachments) && attachments.length ? attachments : []
|
|
1257
1535
|
};
|
|
1258
1536
|
|
|
1259
1537
|
const response = await this.sendAndWait(Opcode.MSG_EDIT, payload);
|
|
@@ -1354,6 +1632,360 @@ class WebMaxClient extends EventEmitter {
|
|
|
1354
1632
|
return messages.map(msg => new Message(msg, this));
|
|
1355
1633
|
}
|
|
1356
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
|
+
|
|
1357
1989
|
/**
|
|
1358
1990
|
* Выполнение зарегистрированных обработчиков
|
|
1359
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/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
|
|