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/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
+