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 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, // ID сообщения для ответа (опционально)
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",
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