webmaxsocket 1.0.0
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 +411 -0
- package/example.js +120 -0
- package/index.js +26 -0
- package/lib/client.js +842 -0
- package/lib/constants.js +36 -0
- package/lib/entities/ChatAction.js +39 -0
- package/lib/entities/Message.js +152 -0
- package/lib/entities/User.js +51 -0
- package/lib/entities/index.js +10 -0
- package/lib/opcodes.js +77 -0
- package/lib/session.js +123 -0
- package/lib/userAgent.js +82 -0
- package/package.json +51 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const EventEmitter = require('events');
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
const qrcode = require('qrcode-terminal');
|
|
5
|
+
const SessionManager = require('./session');
|
|
6
|
+
const { Message, ChatAction, User } = require('./entities');
|
|
7
|
+
const { EventTypes, ChatActions } = require('./constants');
|
|
8
|
+
const { Opcode, DeviceType, getOpcodeName } = require('./opcodes');
|
|
9
|
+
const { UserAgentPayload } = require('./userAgent');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Основной клиент для работы с API Max
|
|
13
|
+
*/
|
|
14
|
+
class WebMaxClient extends EventEmitter {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
super();
|
|
17
|
+
|
|
18
|
+
this.phone = options.phone || null;
|
|
19
|
+
this.sessionName = options.name || options.session || 'default';
|
|
20
|
+
this.apiUrl = options.apiUrl || 'wss://ws-api.oneme.ru/websocket';
|
|
21
|
+
this.origin = 'https://web.max.ru';
|
|
22
|
+
this.session = new SessionManager(this.sessionName);
|
|
23
|
+
|
|
24
|
+
// UserAgent
|
|
25
|
+
this.userAgent = options.userAgent || new UserAgentPayload({
|
|
26
|
+
appVersion: options.appVersion || '25.12.14'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Device ID
|
|
30
|
+
this.deviceId = options.deviceId || this.session.get('deviceId') || uuidv4();
|
|
31
|
+
if (!this.session.get('deviceId')) {
|
|
32
|
+
this.session.set('deviceId', this.deviceId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.ws = null;
|
|
36
|
+
this.me = null;
|
|
37
|
+
this.isConnected = false;
|
|
38
|
+
this.isAuthorized = false;
|
|
39
|
+
this.reconnectAttempts = 0;
|
|
40
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
41
|
+
this.reconnectDelay = options.reconnectDelay || 3000;
|
|
42
|
+
|
|
43
|
+
// Protocol fields
|
|
44
|
+
this.seq = 0;
|
|
45
|
+
this.ver = 11;
|
|
46
|
+
|
|
47
|
+
this.handlers = {
|
|
48
|
+
[EventTypes.START]: [],
|
|
49
|
+
[EventTypes.MESSAGE]: [],
|
|
50
|
+
[EventTypes.MESSAGE_REMOVED]: [],
|
|
51
|
+
[EventTypes.CHAT_ACTION]: [],
|
|
52
|
+
[EventTypes.ERROR]: [],
|
|
53
|
+
[EventTypes.DISCONNECT]: []
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.messageQueue = [];
|
|
57
|
+
this.pendingRequests = new Map();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Регистрация обработчика события start
|
|
62
|
+
*/
|
|
63
|
+
onStart(handler) {
|
|
64
|
+
if (typeof handler === 'function') {
|
|
65
|
+
this.handlers[EventTypes.START].push(handler);
|
|
66
|
+
return handler;
|
|
67
|
+
}
|
|
68
|
+
// Поддержка декоратора
|
|
69
|
+
return (fn) => {
|
|
70
|
+
this.handlers[EventTypes.START].push(fn);
|
|
71
|
+
return fn;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Регистрация обработчика сообщений
|
|
77
|
+
*/
|
|
78
|
+
onMessage(handler) {
|
|
79
|
+
if (typeof handler === 'function') {
|
|
80
|
+
this.handlers[EventTypes.MESSAGE].push(handler);
|
|
81
|
+
return handler;
|
|
82
|
+
}
|
|
83
|
+
return (fn) => {
|
|
84
|
+
this.handlers[EventTypes.MESSAGE].push(fn);
|
|
85
|
+
return fn;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Регистрация обработчика удаленных сообщений
|
|
91
|
+
*/
|
|
92
|
+
onMessageRemoved(handler) {
|
|
93
|
+
if (typeof handler === 'function') {
|
|
94
|
+
this.handlers[EventTypes.MESSAGE_REMOVED].push(handler);
|
|
95
|
+
return handler;
|
|
96
|
+
}
|
|
97
|
+
return (fn) => {
|
|
98
|
+
this.handlers[EventTypes.MESSAGE_REMOVED].push(fn);
|
|
99
|
+
return fn;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Регистрация обработчика действий в чате
|
|
105
|
+
*/
|
|
106
|
+
onChatAction(handler) {
|
|
107
|
+
if (typeof handler === 'function') {
|
|
108
|
+
this.handlers[EventTypes.CHAT_ACTION].push(handler);
|
|
109
|
+
return handler;
|
|
110
|
+
}
|
|
111
|
+
return (fn) => {
|
|
112
|
+
this.handlers[EventTypes.CHAT_ACTION].push(fn);
|
|
113
|
+
return fn;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Регистрация обработчика ошибок
|
|
119
|
+
*/
|
|
120
|
+
onError(handler) {
|
|
121
|
+
if (typeof handler === 'function') {
|
|
122
|
+
this.handlers[EventTypes.ERROR].push(handler);
|
|
123
|
+
return handler;
|
|
124
|
+
}
|
|
125
|
+
return (fn) => {
|
|
126
|
+
this.handlers[EventTypes.ERROR].push(fn);
|
|
127
|
+
return fn;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Запуск клиента
|
|
133
|
+
*/
|
|
134
|
+
async start() {
|
|
135
|
+
try {
|
|
136
|
+
console.log('🚀 Запуск WebMax клиента...');
|
|
137
|
+
|
|
138
|
+
// Подключаемся к WebSocket
|
|
139
|
+
await this.connect();
|
|
140
|
+
|
|
141
|
+
// Проверяем наличие сохраненного токена
|
|
142
|
+
const savedToken = this.session.get('token');
|
|
143
|
+
|
|
144
|
+
if (savedToken) {
|
|
145
|
+
console.log('✅ Найдена сохраненная сессия');
|
|
146
|
+
this._token = savedToken;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await this.sync();
|
|
150
|
+
this.isAuthorized = true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.log('⚠️ Сессия истекла, требуется повторная авторизация');
|
|
153
|
+
this.session.clear();
|
|
154
|
+
await this.authorize();
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
console.log('📱 Требуется авторизация');
|
|
158
|
+
await this.authorize();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Запускаем обработчики start
|
|
162
|
+
await this.triggerHandlers(EventTypes.START);
|
|
163
|
+
|
|
164
|
+
console.log('\n✅ Клиент запущен успешно!');
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('❌ Ошибка при запуске клиента:', error);
|
|
168
|
+
await this.triggerHandlers(EventTypes.ERROR, error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Запрос QR-кода для авторизации (только для device_type="WEB")
|
|
176
|
+
*/
|
|
177
|
+
async requestQR() {
|
|
178
|
+
console.log('Запрос QR-кода для авторизации...');
|
|
179
|
+
|
|
180
|
+
const response = await this.sendAndWait(Opcode.GET_QR, {});
|
|
181
|
+
|
|
182
|
+
if (response.payload && response.payload.error) {
|
|
183
|
+
throw new Error(`QR request error: ${JSON.stringify(response.payload.error)}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return response.payload;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Проверка статуса QR-кода
|
|
191
|
+
*/
|
|
192
|
+
async checkQRStatus(trackId) {
|
|
193
|
+
const response = await this.sendAndWait(Opcode.GET_QR_STATUS, { trackId });
|
|
194
|
+
|
|
195
|
+
if (response.payload && response.payload.error) {
|
|
196
|
+
throw new Error(`QR status error: ${JSON.stringify(response.payload.error)}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return response.payload;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Завершение авторизации по QR-коду
|
|
204
|
+
*/
|
|
205
|
+
async loginByQR(trackId) {
|
|
206
|
+
const response = await this.sendAndWait(Opcode.LOGIN_BY_QR, { trackId });
|
|
207
|
+
|
|
208
|
+
if (response.payload && response.payload.error) {
|
|
209
|
+
throw new Error(`QR login error: ${JSON.stringify(response.payload.error)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return response.payload;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Опрос статуса QR-кода
|
|
217
|
+
*/
|
|
218
|
+
async pollQRStatus(trackId, pollingInterval, expiresAt) {
|
|
219
|
+
console.log('Ожидание сканирования QR-кода...');
|
|
220
|
+
|
|
221
|
+
while (true) {
|
|
222
|
+
// Проверяем не истек ли QR-код
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (now >= expiresAt) {
|
|
225
|
+
throw new Error('QR-код истек. Перезапустите бот для получения нового.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Ждем указанный интервал
|
|
229
|
+
await new Promise(resolve => setTimeout(resolve, pollingInterval));
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const statusResponse = await this.checkQRStatus(trackId);
|
|
233
|
+
|
|
234
|
+
if (statusResponse.status && statusResponse.status.loginAvailable) {
|
|
235
|
+
console.log('✅ QR-код отсканирован!');
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Продолжаем опрос
|
|
240
|
+
process.stdout.write('.');
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error('\nОшибка при проверке статуса QR:', error.message);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Авторизация через QR-код
|
|
251
|
+
*/
|
|
252
|
+
async authorizeByQR() {
|
|
253
|
+
try {
|
|
254
|
+
console.log('Запрос QR-кода для авторизации...');
|
|
255
|
+
|
|
256
|
+
const qrData = await this.requestQR();
|
|
257
|
+
|
|
258
|
+
if (!qrData.qrLink || !qrData.trackId || !qrData.pollingInterval || !qrData.expiresAt) {
|
|
259
|
+
throw new Error('Неполные данные QR-кода от сервера');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log('\n' + '='.repeat(70));
|
|
263
|
+
console.log('🔐 АВТОРИЗАЦИЯ ЧЕРЕЗ QR-КОД');
|
|
264
|
+
console.log('='.repeat(70));
|
|
265
|
+
console.log('\n📱 Откройте приложение Max на телефоне');
|
|
266
|
+
console.log('➡️ Настройки → Устройства → Подключить устройство');
|
|
267
|
+
console.log('📸 Отсканируйте QR-код ниже:\n');
|
|
268
|
+
|
|
269
|
+
// Отображаем QR-код в консоли
|
|
270
|
+
qrcode.generate(qrData.qrLink, { small: true }, (qrCode) => {
|
|
271
|
+
console.log(qrCode);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
console.log('\n💡 Или откройте ссылку: ' + qrData.qrLink);
|
|
275
|
+
console.log('='.repeat(70) + '\n');
|
|
276
|
+
|
|
277
|
+
// Опрашиваем статус
|
|
278
|
+
await this.pollQRStatus(qrData.trackId, qrData.pollingInterval, qrData.expiresAt);
|
|
279
|
+
|
|
280
|
+
// Получаем токен
|
|
281
|
+
console.log('\n\nПолучение токена авторизации...');
|
|
282
|
+
const loginData = await this.loginByQR(qrData.trackId);
|
|
283
|
+
|
|
284
|
+
const loginAttrs = loginData.tokenAttrs && loginData.tokenAttrs.LOGIN;
|
|
285
|
+
const token = loginAttrs && loginAttrs.token;
|
|
286
|
+
|
|
287
|
+
if (!token) {
|
|
288
|
+
throw new Error('Токен не получен из ответа');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.session.set('token', token);
|
|
292
|
+
this.session.set('deviceId', this.deviceId);
|
|
293
|
+
this.isAuthorized = true;
|
|
294
|
+
this._token = token;
|
|
295
|
+
|
|
296
|
+
console.log('✅ Авторизация через QR-код успешна!');
|
|
297
|
+
|
|
298
|
+
// Выполняем sync
|
|
299
|
+
await this.sync();
|
|
300
|
+
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('Ошибка QR авторизации:', error);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Авторизация пользователя через QR-код
|
|
309
|
+
*/
|
|
310
|
+
async authorize() {
|
|
311
|
+
console.log('🔐 Авторизация через QR-код');
|
|
312
|
+
await this.authorizeByQR();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Синхронизация с сервером (получение данных о пользователе, чатах и т.д.)
|
|
318
|
+
*/
|
|
319
|
+
async sync() {
|
|
320
|
+
console.log('🔄 Синхронизация с сервером...');
|
|
321
|
+
|
|
322
|
+
const token = this._token || this.session.get('token');
|
|
323
|
+
|
|
324
|
+
if (!token) {
|
|
325
|
+
throw new Error('Токен не найден, требуется авторизация');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const payload = {
|
|
329
|
+
interactive: true,
|
|
330
|
+
token: token,
|
|
331
|
+
chatsSync: 0,
|
|
332
|
+
contactsSync: 0,
|
|
333
|
+
presenceSync: 0,
|
|
334
|
+
draftsSync: 0,
|
|
335
|
+
chatsCount: 40,
|
|
336
|
+
userAgent: this.userAgent.toJSON()
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const response = await this.sendAndWait(Opcode.LOGIN, payload);
|
|
340
|
+
|
|
341
|
+
if (response.payload && response.payload.error) {
|
|
342
|
+
throw new Error(`Sync error: ${JSON.stringify(response.payload.error)}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Сохраняем информацию о пользователе
|
|
346
|
+
const responsePayload = response.payload || {};
|
|
347
|
+
|
|
348
|
+
// Извлекаем данные пользователя из profile.contact
|
|
349
|
+
if (responsePayload.profile && responsePayload.profile.contact) {
|
|
350
|
+
const contact = responsePayload.profile.contact;
|
|
351
|
+
const name = contact.names && contact.names.length > 0 ? contact.names[0] : {};
|
|
352
|
+
|
|
353
|
+
const userData = {
|
|
354
|
+
id: contact.id,
|
|
355
|
+
firstname: name.firstName || name.name || '',
|
|
356
|
+
lastname: name.lastName || '',
|
|
357
|
+
phone: contact.phone,
|
|
358
|
+
avatar: contact.baseUrl || contact.baseRawUrl,
|
|
359
|
+
photoId: contact.photoId,
|
|
360
|
+
rawData: contact
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
this.me = new User(userData);
|
|
364
|
+
const fullName = this.me.fullname || this.me.firstname || 'User';
|
|
365
|
+
console.log(`✅ Синхронизация завершена. Вы вошли как: ${fullName} (ID: ${this.me.id})`);
|
|
366
|
+
} else {
|
|
367
|
+
console.log('⚠️ Данные пользователя не найдены в ответе sync');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return responsePayload;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Получение информации о текущем пользователе
|
|
375
|
+
*/
|
|
376
|
+
async fetchMyProfile() {
|
|
377
|
+
try {
|
|
378
|
+
console.log('📱 Запрос профиля пользователя...');
|
|
379
|
+
const response = await this.sendAndWait(Opcode.PROFILE, {});
|
|
380
|
+
|
|
381
|
+
if (response.payload && response.payload.user) {
|
|
382
|
+
this.me = new User(response.payload.user);
|
|
383
|
+
const name = this.me.fullname || this.me.firstname || 'User';
|
|
384
|
+
console.log(`✅ Профиль загружен: ${name} (ID: ${this.me.id})`);
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error('⚠️ Не удалось загрузить профиль:', error.message);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Подключение с существующей сессией
|
|
393
|
+
*/
|
|
394
|
+
async connectWithSession() {
|
|
395
|
+
try {
|
|
396
|
+
await this.connect();
|
|
397
|
+
|
|
398
|
+
const token = this.session.get('token');
|
|
399
|
+
|
|
400
|
+
if (!token) {
|
|
401
|
+
console.log('Токен не найден, требуется авторизация');
|
|
402
|
+
await this.authorize();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this._token = token;
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await this.sync();
|
|
410
|
+
this.isAuthorized = true;
|
|
411
|
+
console.log('Подключение с сохраненной сессией успешно');
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.log('Сессия истекла, требуется повторная авторизация');
|
|
414
|
+
this.session.clear();
|
|
415
|
+
await this.authorize();
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Установка WebSocket соединения
|
|
425
|
+
*/
|
|
426
|
+
async connect() {
|
|
427
|
+
if (this.ws && this.isConnected) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return new Promise((resolve, reject) => {
|
|
432
|
+
const headers = {
|
|
433
|
+
'User-Agent': this.userAgent.headerUserAgent,
|
|
434
|
+
'Origin': this.origin
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
this.ws = new WebSocket(this.apiUrl, {
|
|
438
|
+
headers: headers
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
this.ws.on('open', async () => {
|
|
442
|
+
console.log('WebSocket соединение установлено');
|
|
443
|
+
this.isConnected = true;
|
|
444
|
+
this.reconnectAttempts = 0;
|
|
445
|
+
this.emit('connected');
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
// Выполняем handshake
|
|
449
|
+
await this.handshake();
|
|
450
|
+
resolve();
|
|
451
|
+
} catch (error) {
|
|
452
|
+
reject(error);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
this.ws.on('message', (data) => {
|
|
457
|
+
this.handleMessage(data);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
this.ws.on('error', (error) => {
|
|
461
|
+
console.error('WebSocket ошибка:', error.message);
|
|
462
|
+
this.triggerHandlers(EventTypes.ERROR, error);
|
|
463
|
+
reject(error);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
this.ws.on('close', () => {
|
|
467
|
+
console.log('WebSocket соединение закрыто');
|
|
468
|
+
this.isConnected = false;
|
|
469
|
+
this.triggerHandlers(EventTypes.DISCONNECT);
|
|
470
|
+
this.handleReconnect();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Handshake после подключения
|
|
477
|
+
*/
|
|
478
|
+
async handshake() {
|
|
479
|
+
console.log('Выполняется handshake...');
|
|
480
|
+
|
|
481
|
+
const payload = {
|
|
482
|
+
deviceId: this.deviceId,
|
|
483
|
+
userAgent: this.userAgent.toJSON()
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const response = await this.sendAndWait(Opcode.SESSION_INIT, payload);
|
|
487
|
+
|
|
488
|
+
if (response.payload && response.payload.error) {
|
|
489
|
+
throw new Error(`Handshake error: ${JSON.stringify(response.payload.error)}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log('Handshake выполнен успешно');
|
|
493
|
+
return response;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Обработка переподключения
|
|
498
|
+
*/
|
|
499
|
+
handleReconnect() {
|
|
500
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
501
|
+
this.reconnectAttempts++;
|
|
502
|
+
console.log(`Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
|
|
503
|
+
|
|
504
|
+
setTimeout(() => {
|
|
505
|
+
this.connect();
|
|
506
|
+
}, this.reconnectDelay);
|
|
507
|
+
} else {
|
|
508
|
+
console.error('Превышено максимальное количество попыток переподключения');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Обработка входящих сообщений
|
|
514
|
+
*/
|
|
515
|
+
async handleMessage(data) {
|
|
516
|
+
try {
|
|
517
|
+
const message = JSON.parse(data.toString());
|
|
518
|
+
|
|
519
|
+
// Отладочное логирование (раскомментируйте при необходимости)
|
|
520
|
+
// if (message.opcode !== Opcode.PING) {
|
|
521
|
+
// console.log(`📥 Получено: ${getOpcodeName(message.opcode)} (seq=${message.seq})`);
|
|
522
|
+
// }
|
|
523
|
+
|
|
524
|
+
// Обработка ответов на запросы по seq
|
|
525
|
+
if (message.seq && this.pendingRequests.has(message.seq)) {
|
|
526
|
+
const pending = this.pendingRequests.get(message.seq);
|
|
527
|
+
this.pendingRequests.delete(message.seq);
|
|
528
|
+
|
|
529
|
+
if (pending.timeoutId) {
|
|
530
|
+
clearTimeout(pending.timeoutId);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
pending.resolve(message);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Обработка уведомлений
|
|
538
|
+
switch (message.opcode) {
|
|
539
|
+
case Opcode.NOTIF_MESSAGE:
|
|
540
|
+
await this.handleNewMessage(message.payload);
|
|
541
|
+
break;
|
|
542
|
+
|
|
543
|
+
case Opcode.NOTIF_MSG_DELETE:
|
|
544
|
+
await this.handleRemovedMessage(message.payload);
|
|
545
|
+
break;
|
|
546
|
+
|
|
547
|
+
case Opcode.NOTIF_CHAT:
|
|
548
|
+
await this.handleChatAction(message.payload);
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
case Opcode.PING:
|
|
552
|
+
// Отвечаем на ping (без логирования)
|
|
553
|
+
await this.sendPong();
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
default:
|
|
557
|
+
this.emit('raw_message', message);
|
|
558
|
+
}
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.error('Ошибка при обработке сообщения:', error);
|
|
561
|
+
await this.triggerHandlers(EventTypes.ERROR, error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Отправка pong ответа на ping
|
|
567
|
+
*/
|
|
568
|
+
async sendPong() {
|
|
569
|
+
try {
|
|
570
|
+
const message = this.makeMessage(Opcode.PING, {});
|
|
571
|
+
this.ws.send(JSON.stringify(message));
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error('Ошибка при отправке pong:', error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Обработка нового сообщения
|
|
579
|
+
*/
|
|
580
|
+
async handleNewMessage(data) {
|
|
581
|
+
// Извлекаем данные сообщения из правильного места
|
|
582
|
+
// Структура: { chatId, message: { sender, id, text, ... } }
|
|
583
|
+
const messageData = data.message || data;
|
|
584
|
+
|
|
585
|
+
// Добавляем chatId если его нет в messageData
|
|
586
|
+
if (!messageData.chatId && data.chatId) {
|
|
587
|
+
messageData.chatId = data.chatId;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const message = new Message(messageData, this);
|
|
591
|
+
|
|
592
|
+
// Попытка загрузить информацию об отправителе если её нет
|
|
593
|
+
if (!message.sender && message.senderId && message.senderId !== this.me?.id) {
|
|
594
|
+
await message.fetchSender();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
await this.triggerHandlers(EventTypes.MESSAGE, message);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Обработка удаленного сообщения
|
|
602
|
+
*/
|
|
603
|
+
async handleRemovedMessage(data) {
|
|
604
|
+
const message = new Message(data, this);
|
|
605
|
+
await this.triggerHandlers(EventTypes.MESSAGE_REMOVED, message);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Обработка действия в чате
|
|
610
|
+
*/
|
|
611
|
+
async handleChatAction(data) {
|
|
612
|
+
const action = new ChatAction(data, this);
|
|
613
|
+
await this.triggerHandlers(EventTypes.CHAT_ACTION, action);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Создает сообщение в протоколе Max API
|
|
618
|
+
*/
|
|
619
|
+
makeMessage(opcode, payload, cmd = 0) {
|
|
620
|
+
this.seq += 1;
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
ver: this.ver,
|
|
624
|
+
cmd: cmd,
|
|
625
|
+
seq: this.seq,
|
|
626
|
+
opcode: opcode,
|
|
627
|
+
payload: payload
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Отправка запроса через WebSocket и ожидание ответа
|
|
633
|
+
*/
|
|
634
|
+
sendAndWait(opcode, payload, cmd = 0, timeout = 20000) {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
if (!this.isConnected) {
|
|
637
|
+
reject(new Error('WebSocket не подключен'));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const message = this.makeMessage(opcode, payload, cmd);
|
|
642
|
+
const seq = message.seq;
|
|
643
|
+
|
|
644
|
+
this.pendingRequests.set(seq, { resolve, reject });
|
|
645
|
+
|
|
646
|
+
// Таймаут для запроса
|
|
647
|
+
const timeoutId = setTimeout(() => {
|
|
648
|
+
if (this.pendingRequests.has(seq)) {
|
|
649
|
+
this.pendingRequests.delete(seq);
|
|
650
|
+
reject(new Error(`Таймаут запроса (seq: ${seq}, opcode: ${opcode})`));
|
|
651
|
+
}
|
|
652
|
+
}, timeout);
|
|
653
|
+
|
|
654
|
+
// Сохраняем timeoutId чтобы можно было отменить
|
|
655
|
+
this.pendingRequests.get(seq).timeoutId = timeoutId;
|
|
656
|
+
|
|
657
|
+
// Отладочное логирование (раскомментируйте при необходимости)
|
|
658
|
+
// if (opcode !== Opcode.PING) {
|
|
659
|
+
// console.log(`📤 Отправка: ${getOpcodeName(opcode)} (seq=${seq})`);
|
|
660
|
+
// }
|
|
661
|
+
this.ws.send(JSON.stringify(message));
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Отправка сообщения
|
|
667
|
+
*/
|
|
668
|
+
async sendMessage(options) {
|
|
669
|
+
if (typeof options === 'string') {
|
|
670
|
+
throw new Error('sendMessage требует объект с параметрами: { chatId, text, cid }');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const { chatId, text, cid, replyTo, attachments } = options;
|
|
674
|
+
|
|
675
|
+
const payload = {
|
|
676
|
+
chatId: chatId,
|
|
677
|
+
message: {
|
|
678
|
+
text: text || '',
|
|
679
|
+
cid: cid || Date.now(),
|
|
680
|
+
elements: [],
|
|
681
|
+
attaches: attachments || [],
|
|
682
|
+
link: replyTo ? { type: 'REPLY', messageId: replyTo } : null
|
|
683
|
+
},
|
|
684
|
+
notify: false
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const response = await this.sendAndWait(Opcode.MSG_SEND, payload);
|
|
688
|
+
|
|
689
|
+
if (response.payload && response.payload.message) {
|
|
690
|
+
return new Message(response.payload.message, this);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return response.payload;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Редактирование сообщения
|
|
698
|
+
*/
|
|
699
|
+
async editMessage(options) {
|
|
700
|
+
const { messageId, chatId, text } = options;
|
|
701
|
+
|
|
702
|
+
const payload = {
|
|
703
|
+
chatId: chatId,
|
|
704
|
+
messageId: messageId,
|
|
705
|
+
text: text || '',
|
|
706
|
+
elements: [],
|
|
707
|
+
attaches: []
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const response = await this.sendAndWait(Opcode.MSG_EDIT, payload);
|
|
711
|
+
|
|
712
|
+
if (response.payload && response.payload.message) {
|
|
713
|
+
return new Message(response.payload.message, this);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return response.payload;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Удаление сообщения
|
|
721
|
+
*/
|
|
722
|
+
async deleteMessage(options) {
|
|
723
|
+
const { messageId, chatId, forMe } = options;
|
|
724
|
+
|
|
725
|
+
const payload = {
|
|
726
|
+
chatId: chatId,
|
|
727
|
+
messageIds: Array.isArray(messageId) ? messageId : [messageId],
|
|
728
|
+
forMe: forMe || false
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
await this.sendAndWait(Opcode.MSG_DELETE, payload);
|
|
732
|
+
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Получение информации о пользователе по ID
|
|
738
|
+
*/
|
|
739
|
+
async getUser(userId) {
|
|
740
|
+
const payload = {
|
|
741
|
+
contactIds: [userId]
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const response = await this.sendAndWait(Opcode.CONTACT_INFO, payload);
|
|
745
|
+
|
|
746
|
+
if (response.payload && response.payload.contacts && response.payload.contacts.length > 0) {
|
|
747
|
+
const contact = response.payload.contacts[0];
|
|
748
|
+
|
|
749
|
+
// Преобразуем структуру контакта в понятный User формат
|
|
750
|
+
const name = contact.names && contact.names.length > 0 ? contact.names[0] : {};
|
|
751
|
+
|
|
752
|
+
const userData = {
|
|
753
|
+
id: contact.id,
|
|
754
|
+
firstname: name.firstName || name.name || '',
|
|
755
|
+
lastname: name.lastName || '',
|
|
756
|
+
phone: contact.phone,
|
|
757
|
+
avatar: contact.baseUrl || contact.baseRawUrl,
|
|
758
|
+
photoId: contact.photoId,
|
|
759
|
+
rawData: contact
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
return new User(userData);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Получение списка чатов
|
|
770
|
+
*/
|
|
771
|
+
async getChats(marker = 0) {
|
|
772
|
+
const payload = {
|
|
773
|
+
marker: marker
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const response = await this.sendAndWait(Opcode.CHATS_LIST, payload);
|
|
777
|
+
|
|
778
|
+
return response.payload && response.payload.chats ? response.payload.chats : [];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Получение истории сообщений
|
|
783
|
+
*/
|
|
784
|
+
async getHistory(chatId, from = Date.now(), backward = 200, forward = 0) {
|
|
785
|
+
const payload = {
|
|
786
|
+
chatId: chatId,
|
|
787
|
+
from: from,
|
|
788
|
+
forward: forward,
|
|
789
|
+
backward: backward,
|
|
790
|
+
getMessages: true
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const response = await this.sendAndWait(Opcode.CHAT_HISTORY, payload);
|
|
794
|
+
|
|
795
|
+
const messages = response.payload && response.payload.messages ? response.payload.messages : [];
|
|
796
|
+
return messages.map(msg => new Message(msg, this));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Выполнение зарегистрированных обработчиков
|
|
801
|
+
*/
|
|
802
|
+
async triggerHandlers(eventType, data = null) {
|
|
803
|
+
const handlers = this.handlers[eventType] || [];
|
|
804
|
+
|
|
805
|
+
for (const handler of handlers) {
|
|
806
|
+
try {
|
|
807
|
+
if (data !== null) {
|
|
808
|
+
await handler(data);
|
|
809
|
+
} else {
|
|
810
|
+
await handler();
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.error(`Ошибка в обработчике ${eventType}:`, error);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Остановка клиента
|
|
820
|
+
*/
|
|
821
|
+
async stop() {
|
|
822
|
+
if (this.ws) {
|
|
823
|
+
this.ws.close();
|
|
824
|
+
this.ws = null;
|
|
825
|
+
}
|
|
826
|
+
this.isConnected = false;
|
|
827
|
+
this.isAuthorized = false;
|
|
828
|
+
console.log('Клиент остановлен');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Выход из аккаунта
|
|
833
|
+
*/
|
|
834
|
+
async logout() {
|
|
835
|
+
await this.stop();
|
|
836
|
+
this.session.destroy();
|
|
837
|
+
console.log('Выход выполнен, сессия удалена');
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
module.exports = WebMaxClient;
|
|
842
|
+
|