webmaxsocket 1.1.3 → 1.1.5
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/index.js +3 -1
- package/lib/client.js +1128 -27
- package/lib/opcodes.js +3 -8
- package/lib/qrWebLogin.js +59 -0
- package/lib/socketTransport.js +3 -1
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -11,6 +11,7 @@ const { EventTypes, ChatActions } = require('./constants');
|
|
|
11
11
|
const { Opcode, DeviceType, getOpcodeName } = require('./opcodes');
|
|
12
12
|
const { UserAgentPayload } = require('./userAgent');
|
|
13
13
|
const { resolveIncomingLogMode, printIncomingLog } = require('./incomingLog');
|
|
14
|
+
const { parseQrTrackId } = require('./qrWebLogin');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Загружает конфиг: { token, agent }
|
|
@@ -79,34 +80,50 @@ class WebMaxClient extends EventEmitter {
|
|
|
79
80
|
|
|
80
81
|
this._providedToken = token;
|
|
81
82
|
this._saveTokenToSession = options.saveToken !== false;
|
|
82
|
-
this.origin = 'https://web.max.ru';
|
|
83
|
+
this.origin = options.origin || 'https://web.max.ru';
|
|
84
|
+
/** Доп. заголовок для ws (например Referer при QR с max.ru, пока Origin остаётся web.max.ru — иначе 403). */
|
|
85
|
+
this._wsReferer = options.referer || options.wsReferer || null;
|
|
83
86
|
this.session = new SessionManager(this.sessionName);
|
|
84
87
|
|
|
85
88
|
const deviceTypeMap = { 1: 'WEB', 2: 'IOS', 3: 'ANDROID' };
|
|
86
89
|
const rawDeviceType = options.deviceType ?? configObj.device_type ?? configObj.deviceType ?? this.session.get('deviceType');
|
|
87
90
|
const deviceType = deviceTypeMap[rawDeviceType] || rawDeviceType || 'WEB';
|
|
88
|
-
const uaString =
|
|
91
|
+
const uaString =
|
|
92
|
+
agent ||
|
|
93
|
+
configObj.headerUserAgent ||
|
|
94
|
+
configObj.ua ||
|
|
95
|
+
this.session.get('headerUserAgent') ||
|
|
96
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
|
|
89
97
|
const webDefaults = {
|
|
90
98
|
deviceType: deviceType,
|
|
91
|
-
locale: options.locale || configObj.locale || 'ru',
|
|
92
|
-
deviceLocale:
|
|
99
|
+
locale: options.locale || configObj.locale || this.session.get('locale') || 'ru',
|
|
100
|
+
deviceLocale:
|
|
101
|
+
options.deviceLocale ||
|
|
102
|
+
configObj.deviceLocale ||
|
|
103
|
+
configObj.locale ||
|
|
104
|
+
this.session.get('deviceLocale') ||
|
|
105
|
+
this.session.get('locale') ||
|
|
106
|
+
'ru',
|
|
93
107
|
osVersion:
|
|
94
108
|
options.osVersion ||
|
|
95
109
|
configObj.osVersion ||
|
|
110
|
+
this.session.get('osVersion') ||
|
|
96
111
|
(deviceType === 'IOS' ? '18.6.2' : deviceType === 'ANDROID' ? '14' : 'Windows 11'),
|
|
97
112
|
deviceName:
|
|
98
113
|
options.deviceName ||
|
|
99
114
|
configObj.deviceName ||
|
|
115
|
+
this.session.get('deviceName') ||
|
|
100
116
|
(deviceType === 'IOS' ? 'Safari' : deviceType === 'ANDROID' ? 'Chrome' : 'Chrome'),
|
|
101
117
|
headerUserAgent: options.headerUserAgent || options.ua || uaString,
|
|
102
118
|
// Ниже 25.12.13 сервер может отвечать qr_login.disabled на GET_QR (см. PyMax _login).
|
|
103
|
-
appVersion: options.appVersion || configObj.appVersion || '
|
|
119
|
+
appVersion: options.appVersion || configObj.appVersion || this.session.get('appVersion') || '26.3.9',
|
|
104
120
|
screen:
|
|
105
121
|
options.screen ||
|
|
106
122
|
configObj.screen ||
|
|
123
|
+
this.session.get('screen') ||
|
|
107
124
|
(deviceType === 'IOS' ? '390x844 3.0x' : deviceType === 'ANDROID' ? '360x780 3.0x' : '1080x1920 1.0x'),
|
|
108
|
-
timezone: options.timezone || configObj.timezone || 'Europe/Moscow',
|
|
109
|
-
buildNumber: options.buildNumber ?? configObj.buildNumber,
|
|
125
|
+
timezone: options.timezone || configObj.timezone || this.session.get('timezone') || 'Europe/Moscow',
|
|
126
|
+
buildNumber: options.buildNumber ?? configObj.buildNumber ?? this.session.get('buildNumber'),
|
|
110
127
|
clientSessionId: options.clientSessionId ?? configObj.clientSessionId ?? this.session.get('clientSessionId'),
|
|
111
128
|
release: options.release ?? configObj.release
|
|
112
129
|
};
|
|
@@ -127,8 +144,8 @@ class WebMaxClient extends EventEmitter {
|
|
|
127
144
|
this.isConnected = false;
|
|
128
145
|
this.isAuthorized = false;
|
|
129
146
|
this.reconnectAttempts = 0;
|
|
130
|
-
this.maxReconnectAttempts = options.maxReconnectAttempts
|
|
131
|
-
this.reconnectDelay = options.reconnectDelay
|
|
147
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
148
|
+
this.reconnectDelay = options.reconnectDelay ?? 3000;
|
|
132
149
|
|
|
133
150
|
// Protocol fields
|
|
134
151
|
this.seq = 0;
|
|
@@ -154,6 +171,9 @@ class WebMaxClient extends EventEmitter {
|
|
|
154
171
|
this._wireIncomingLogListeners();
|
|
155
172
|
/** client id локальных исходящих сообщений (int32, часто ждут валидацию на сервере) */
|
|
156
173
|
this._clientSendCid = 1 + Math.floor(Math.random() * 0xfffff);
|
|
174
|
+
/** После HTTP POST видео/файла — ждём NOTIF_ATTACH */
|
|
175
|
+
this._uploadPendingVideo = new Map();
|
|
176
|
+
this._uploadPendingFile = new Map();
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
/**
|
|
@@ -313,7 +333,7 @@ class WebMaxClient extends EventEmitter {
|
|
|
313
333
|
async requestQR() {
|
|
314
334
|
console.log('Запрос QR-кода для авторизации...');
|
|
315
335
|
|
|
316
|
-
const response = await this.sendAndWait(Opcode.GET_QR,
|
|
336
|
+
const response = await this.sendAndWait(Opcode.GET_QR, undefined);
|
|
317
337
|
|
|
318
338
|
throwIfGetQRRejected(response.payload);
|
|
319
339
|
|
|
@@ -333,19 +353,456 @@ class WebMaxClient extends EventEmitter {
|
|
|
333
353
|
return response.payload;
|
|
334
354
|
}
|
|
335
355
|
|
|
356
|
+
/**
|
|
357
|
+
* На TCP после ответа LOGIN сервер может закрыть сокет — перед следующим RPC переподключаемся и снова LOGIN.
|
|
358
|
+
*/
|
|
359
|
+
async _ensureTcpSocketReadyForRpc() {
|
|
360
|
+
const st = this._socketTransport;
|
|
361
|
+
if (st && st.socket && !st.socket.destroyed) return;
|
|
362
|
+
const token = this._token || this.session.get('token');
|
|
363
|
+
if (!token) {
|
|
364
|
+
throw new Error('Нет токена для переподключения TCP');
|
|
365
|
+
}
|
|
366
|
+
console.log('Переподключение TCP (сокет закрыт после предыдущего запроса)…');
|
|
367
|
+
this.isConnected = false;
|
|
368
|
+
await this.connect();
|
|
369
|
+
this._token = token;
|
|
370
|
+
await this.sync();
|
|
371
|
+
this.isAuthorized = true;
|
|
372
|
+
}
|
|
373
|
+
|
|
336
374
|
/**
|
|
337
375
|
* Завершение авторизации по QR-коду
|
|
376
|
+
* @param {string} trackId — UUID (веб GET_QR) или opaque token `qr_…` (max.ru/qr/v1/auth)
|
|
338
377
|
*/
|
|
339
378
|
async loginByQR(trackId) {
|
|
340
|
-
|
|
341
|
-
|
|
379
|
+
if (this._useSocketTransport) {
|
|
380
|
+
await this._ensureTcpSocketReadyForRpc();
|
|
381
|
+
}
|
|
382
|
+
const id = String(trackId).trim();
|
|
383
|
+
const payload = id.startsWith('qr_') ? { token: id } : { trackId: id };
|
|
384
|
+
const response = await this.sendAndWait(Opcode.LOGIN_BY_QR, payload);
|
|
385
|
+
|
|
342
386
|
if (response.payload && response.payload.error) {
|
|
343
387
|
throw new Error(`QR login error: ${JSON.stringify(response.payload.error)}`);
|
|
344
388
|
}
|
|
345
|
-
|
|
389
|
+
|
|
346
390
|
return response.payload;
|
|
347
391
|
}
|
|
348
392
|
|
|
393
|
+
async approveQR(qrLink) {
|
|
394
|
+
if (this._useSocketTransport) {
|
|
395
|
+
await this._ensureTcpSocketReadyForRpc();
|
|
396
|
+
}
|
|
397
|
+
if (!this.isAuthorized) {
|
|
398
|
+
throw new Error('Требуется авторизованная сессия для одобрения QR');
|
|
399
|
+
}
|
|
400
|
+
const link = String(qrLink).trim();
|
|
401
|
+
const response = await this.sendAndWait(Opcode.AUTH_QR_APPROVE, { qrLink: link });
|
|
402
|
+
if (response.payload && response.payload.error) {
|
|
403
|
+
throw new Error(`QR approve error: ${JSON.stringify(response.payload.error)}`);
|
|
404
|
+
}
|
|
405
|
+
return response.payload;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* LOGIN_BY_QR: как web.max.ru — сначала только trackId (HAR), затем запасные варианты + токен аккаунта.
|
|
410
|
+
*/
|
|
411
|
+
async _loginByQrWebMax(web, trackId, accountToken) {
|
|
412
|
+
const id = String(trackId).trim();
|
|
413
|
+
if (id.startsWith('qr_')) {
|
|
414
|
+
const response = await web.sendAndWait(Opcode.LOGIN_BY_QR, { token: id });
|
|
415
|
+
if (response.payload && response.payload.error) {
|
|
416
|
+
throw new Error(`QR login error: ${JSON.stringify(response.payload.error)}`);
|
|
417
|
+
}
|
|
418
|
+
return response.payload;
|
|
419
|
+
}
|
|
420
|
+
const payloads = [{ trackId: id }, { track: id }, { authTrackId: id }];
|
|
421
|
+
if (accountToken) {
|
|
422
|
+
payloads.push(
|
|
423
|
+
{ trackId: id, token: accountToken },
|
|
424
|
+
{ track: id, token: accountToken },
|
|
425
|
+
{ trackId: id, account_token: accountToken },
|
|
426
|
+
{ track: id, account_token: accountToken }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let lastErr;
|
|
431
|
+
for (const payload of payloads) {
|
|
432
|
+
try {
|
|
433
|
+
const response = await web.sendAndWait(Opcode.LOGIN_BY_QR, payload);
|
|
434
|
+
if (response.payload && response.payload.error) {
|
|
435
|
+
const err = response.payload.error;
|
|
436
|
+
const msg =
|
|
437
|
+
typeof err === 'string'
|
|
438
|
+
? err
|
|
439
|
+
: err && typeof err.message === 'string'
|
|
440
|
+
? err.message
|
|
441
|
+
: response.payload.localizedMessage || JSON.stringify(err);
|
|
442
|
+
lastErr = new Error(msg);
|
|
443
|
+
if (/track\.not\.found/i.test(String(msg))) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`${msg}\n` +
|
|
446
|
+
'Трек QR устарел или уже использован. Откройте web.max.ru, дождитесь нового QR и сразу сохраните qr.png.'
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (/proto\.payload/i.test(String(msg))) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
return response.payload;
|
|
455
|
+
} catch (e) {
|
|
456
|
+
lastErr = e;
|
|
457
|
+
if (e && e.message && /track\.not\.found|Трек QR устарел/i.test(e.message)) {
|
|
458
|
+
throw e;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
throw lastErr || new Error('LOGIN_BY_QR не удался');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Как в web.max.ru (HAR): опрос GET_QR_STATUS, пока status.loginAvailable !== true (после скана в приложении Max).
|
|
467
|
+
* Без этого LOGIN_BY_QR часто даёт track.not.found — трек не «готов» к подтверждению.
|
|
468
|
+
*/
|
|
469
|
+
async _waitForWebQrLoginAvailable(web, trackId, options = {}) {
|
|
470
|
+
const pollingInterval = options.pollingInterval ?? 5000;
|
|
471
|
+
const defaultTtlMs = options.defaultTtlMs ?? 180000;
|
|
472
|
+
let expiresAt = Date.now() + defaultTtlMs;
|
|
473
|
+
|
|
474
|
+
console.log(
|
|
475
|
+
'\n📱 Нужно подтверждение с телефона (как в официальном web-клиенте):\n' +
|
|
476
|
+
' Max на телефоне → раздел с входом по QR к веб-версии / устройства — наведите на тот же QR (тот же аккаунт).\n' +
|
|
477
|
+
' Чтобы вкладка браузера не отправила LOGIN_BY_QR раньше скрипта: после сохранения qr.png закройте вкладку web.max.ru,\n' +
|
|
478
|
+
' затем запустите этот скрипт и отсканируйте QR с файла/второго монитора.\n' +
|
|
479
|
+
' Ожидание loginAvailable на сервере…'
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
while (true) {
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
if (now >= expiresAt) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
'Нет loginAvailable до истечения времени: отсканируйте QR приложением на тот же аккаунт или обновите qr.png.'
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const statusResponse = await web.checkQRStatus(trackId);
|
|
491
|
+
if (statusResponse.status && statusResponse.status.expiresAt != null) {
|
|
492
|
+
const ex = Number(statusResponse.status.expiresAt);
|
|
493
|
+
if (Number.isFinite(ex)) {
|
|
494
|
+
expiresAt = Math.min(expiresAt, ex);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (statusResponse.status && statusResponse.status.loginAvailable) {
|
|
498
|
+
console.log('\n✅ Сервер готов к LOGIN_BY_QR (loginAvailable).');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
process.stdout.write('.');
|
|
503
|
+
|
|
504
|
+
const wait = Math.min(
|
|
505
|
+
pollingInterval,
|
|
506
|
+
Math.max(0, expiresAt - Date.now())
|
|
507
|
+
);
|
|
508
|
+
await new Promise((r) => setTimeout(r, wait || pollingInterval));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Подтверждение входа по QR: новое TCP-соединение (только SESSION_INIT), затем LOGIN_BY_QR — без LOGIN на этом сокете.
|
|
514
|
+
*/
|
|
515
|
+
async _approveWebLoginByQrViaEphemeralTcp(trackId) {
|
|
516
|
+
const accountToken = this._token || this.session.get('token');
|
|
517
|
+
const transport = new MaxSocketTransport({
|
|
518
|
+
deviceId: this.deviceId,
|
|
519
|
+
deviceType: this.userAgent.deviceType,
|
|
520
|
+
ua: this.userAgent.headerUserAgent,
|
|
521
|
+
debug: this.debug,
|
|
522
|
+
});
|
|
523
|
+
await transport.connect();
|
|
524
|
+
await transport.handshake(this.userAgent);
|
|
525
|
+
try {
|
|
526
|
+
const id = String(trackId).trim();
|
|
527
|
+
if (id.startsWith('qr_')) {
|
|
528
|
+
const result = await transport.sendAndWait(Opcode.LOGIN_BY_QR, { token: id });
|
|
529
|
+
return result.payload;
|
|
530
|
+
}
|
|
531
|
+
const payloads = [
|
|
532
|
+
{ trackId: id },
|
|
533
|
+
{ track: id },
|
|
534
|
+
{ trackId: id, token: accountToken },
|
|
535
|
+
{ track: id, token: accountToken },
|
|
536
|
+
];
|
|
537
|
+
let lastErr;
|
|
538
|
+
for (const payload of payloads) {
|
|
539
|
+
try {
|
|
540
|
+
const result = await transport.sendAndWait(Opcode.LOGIN_BY_QR, payload);
|
|
541
|
+
return result.payload;
|
|
542
|
+
} catch (e) {
|
|
543
|
+
lastErr = e;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
throw lastErr || new Error('LOGIN_BY_QR (TCP) не удался');
|
|
547
|
+
} finally {
|
|
548
|
+
await transport.close();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Подтверждение входа в web.max по QR при основной сессии на TCP: отдельный WebSocket + LOGIN + LOGIN_BY_QR.
|
|
554
|
+
* На том же TCP после LOGIN сервер не принимает LOGIN_BY_QR («Недопустимое состояние сессии»).
|
|
555
|
+
* WEB-клиент: не используем IOS deviceId (даёт login.cred); берём webDeviceId из сессии или новый UUID.
|
|
556
|
+
* Сначала пробуем webToken для sync, затем основной token. Без успешного sync LOGIN_BY_QR часто даёт auth_by_track.no.attempts.
|
|
557
|
+
*/
|
|
558
|
+
async _approveWebLoginByQrViaEphemeralWeb(trackId, options = {}, qrSource = '') {
|
|
559
|
+
const {
|
|
560
|
+
retry = true,
|
|
561
|
+
saveWebSession = true,
|
|
562
|
+
waitForPhoneScan = true,
|
|
563
|
+
qrPollingInterval = 5000,
|
|
564
|
+
} = options;
|
|
565
|
+
const accountToken = this._token || this.session.get('token');
|
|
566
|
+
if (!accountToken) {
|
|
567
|
+
throw new Error('Нет токена сессии');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const refererForMaxQr =
|
|
571
|
+
/https?:\/\/max\.ru\//i.test(String(qrSource)) && !/web\.max\.ru/i.test(String(qrSource))
|
|
572
|
+
? 'https://max.ru/'
|
|
573
|
+
: null;
|
|
574
|
+
|
|
575
|
+
const webUa =
|
|
576
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36';
|
|
577
|
+
const webDeviceId = this.session.get('webDeviceId') || uuidv4();
|
|
578
|
+
|
|
579
|
+
const ephemeralName = `_web_qr_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
580
|
+
const web = new this.constructor({
|
|
581
|
+
name: ephemeralName,
|
|
582
|
+
deviceType: 'WEB',
|
|
583
|
+
deviceId: webDeviceId,
|
|
584
|
+
// Не делим clientSessionId с параллельным TCP (IOS) — иначе сервер может обрывать WS (1006).
|
|
585
|
+
clientSessionId: Math.floor(Math.random() * 0x7fffffff),
|
|
586
|
+
headerUserAgent: webUa,
|
|
587
|
+
appVersion: '26.3.9',
|
|
588
|
+
screen: '1920x1080 1.0x',
|
|
589
|
+
osVersion: 'Windows 11',
|
|
590
|
+
deviceName: 'Chrome',
|
|
591
|
+
saveToken: false,
|
|
592
|
+
debug: this.debug,
|
|
593
|
+
apiUrl: this.apiUrl,
|
|
594
|
+
origin: 'https://web.max.ru',
|
|
595
|
+
referer: refererForMaxQr,
|
|
596
|
+
maxReconnectAttempts: 0
|
|
597
|
+
});
|
|
598
|
+
web.handleReconnect = () => {};
|
|
599
|
+
|
|
600
|
+
const wrapTrackError = (e) => {
|
|
601
|
+
const m = e && e.message ? e.message : String(e);
|
|
602
|
+
if (/auth_by_track|no\.attempts|Недопустимое состояние|track\.not\.found/i.test(m)) {
|
|
603
|
+
return new Error(
|
|
604
|
+
`${m}\n` +
|
|
605
|
+
'Подсказка: WebSocket принимает только Origin web.max.ru; для max.ru/:auth/ добавлен Referer. Сделайте новый скрин qr.png сразу после появления QR; старый трек даёт track.not.found.'
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return e;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
console.log(
|
|
613
|
+
`Отдельное WebSocket (Origin=web.max.ru${refererForMaxQr ? ', Referer=max.ru' : ''}): ws-api принимает только Origin web.max.ru; при QR с max.ru добавлен Referer.`
|
|
614
|
+
);
|
|
615
|
+
await web.connect();
|
|
616
|
+
|
|
617
|
+
const webTok = this.session.get('webToken');
|
|
618
|
+
let syncOk = false;
|
|
619
|
+
// IOS-токен на WEB даёт login.cred и сервер часто рвёт сокет — не вызываем sync с ним.
|
|
620
|
+
// Только отдельный webToken из прошлого входа в web.max.
|
|
621
|
+
if (webTok && webTok !== accountToken) {
|
|
622
|
+
try {
|
|
623
|
+
web._token = webTok;
|
|
624
|
+
await web.sync();
|
|
625
|
+
syncOk = true;
|
|
626
|
+
web.isAuthorized = true;
|
|
627
|
+
if (saveWebSession && web.deviceId) {
|
|
628
|
+
this.session.set('webDeviceId', web.deviceId);
|
|
629
|
+
}
|
|
630
|
+
} catch (e) {
|
|
631
|
+
const msg = e && e.message ? e.message : String(e);
|
|
632
|
+
console.log(`WEB LOGIN (sync) с webToken: ${msg}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (waitForPhoneScan) {
|
|
637
|
+
await this._waitForWebQrLoginAvailable(web, trackId, {
|
|
638
|
+
pollingInterval: qrPollingInterval,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const attempts = retry ? 2 : 1;
|
|
643
|
+
let lastErr;
|
|
644
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
645
|
+
try {
|
|
646
|
+
const wsOpen = web.ws && web.ws.readyState === WebSocket.OPEN;
|
|
647
|
+
if (!web.isConnected || !wsOpen) {
|
|
648
|
+
console.log('Переподключение WebSocket перед LOGIN_BY_QR (после ошибки LOGIN сокет часто закрывается)…');
|
|
649
|
+
if (web.ws) {
|
|
650
|
+
try {
|
|
651
|
+
web.ws.removeAllListeners();
|
|
652
|
+
} catch (_) {}
|
|
653
|
+
try {
|
|
654
|
+
web.ws.close();
|
|
655
|
+
} catch (_) {}
|
|
656
|
+
web.ws = null;
|
|
657
|
+
}
|
|
658
|
+
web.isConnected = false;
|
|
659
|
+
web.pendingRequests.clear();
|
|
660
|
+
await web.connect();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let loginData;
|
|
664
|
+
if (syncOk) {
|
|
665
|
+
loginData = await this._loginByQrWebMax(web, trackId, null);
|
|
666
|
+
} else {
|
|
667
|
+
loginData = await this._loginByQrWebMax(web, trackId, accountToken);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (saveWebSession && loginData && typeof loginData === 'object') {
|
|
671
|
+
this.session.set('webQrTrackId', trackId);
|
|
672
|
+
const loginAttrs = loginData.tokenAttrs && loginData.tokenAttrs.LOGIN;
|
|
673
|
+
const wtoken = loginAttrs && loginAttrs.token;
|
|
674
|
+
if (wtoken) {
|
|
675
|
+
this.session.set('webToken', wtoken);
|
|
676
|
+
}
|
|
677
|
+
if (loginData.deviceId != null) {
|
|
678
|
+
this.session.set('webDeviceId', loginData.deviceId);
|
|
679
|
+
}
|
|
680
|
+
if (loginData.clientSessionId != null) {
|
|
681
|
+
this.session.set('webClientSessionId', loginData.clientSessionId);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
console.log('✅ Вход в веб-версию по QR подтверждён (LOGIN_BY_QR через WebSocket)');
|
|
686
|
+
return loginData;
|
|
687
|
+
} catch (e) {
|
|
688
|
+
lastErr = wrapTrackError(e);
|
|
689
|
+
if (attempt < attempts) {
|
|
690
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
throw lastErr;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
throw lastErr;
|
|
697
|
+
} finally {
|
|
698
|
+
try {
|
|
699
|
+
await web.stop();
|
|
700
|
+
web.session.destroy();
|
|
701
|
+
} catch (_) {}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Подтвердить вход в веб-версию (web.max.ru) по URL из QR или trackId,
|
|
707
|
+
* используя уже авторизованную сессию (в т.ч. TCP после SMS/токена).
|
|
708
|
+
* Аналог сценария с `qr_url` + `account_token` на сторонних сервисах: здесь токен уже в сессии.
|
|
709
|
+
*
|
|
710
|
+
* @param {string} qrUrlOrTrackId — полный URL из QR, либо UUID trackId
|
|
711
|
+
* @param {{ retry?: boolean, saveWebSession?: boolean, waitForPhoneScan?: boolean, qrPollingInterval?: number }} [options]
|
|
712
|
+
* @returns {Promise<object>} payload ответа LOGIN_BY_QR
|
|
713
|
+
*/
|
|
714
|
+
async approveWebLoginByQr(qrUrlOrTrackId, options = {}) {
|
|
715
|
+
const { retry = true, saveWebSession = true } = options;
|
|
716
|
+
|
|
717
|
+
const trackId = parseQrTrackId(qrUrlOrTrackId);
|
|
718
|
+
if (!trackId) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
'Не удалось извлечь идентификатор из QR: ожидается URL max.ru/qr/v1/auth?token=qr_…, web.max с trackId или UUID'
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const token = this._token || this.session.get('token');
|
|
725
|
+
if (!token) {
|
|
726
|
+
throw new Error('Нет токена в сессии для подтверждения входа');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (this._useSocketTransport) {
|
|
730
|
+
if (!this.isAuthorized) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
'Нужна авторизованная сессия: выполните connect() + sync() с действующим токеном'
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (process.env.MAX_QR_TRY_TCP_FIRST === '1') {
|
|
736
|
+
try {
|
|
737
|
+
const loginData = await this._approveWebLoginByQrViaEphemeralTcp(trackId);
|
|
738
|
+
if (saveWebSession && loginData && typeof loginData === 'object') {
|
|
739
|
+
this.session.set('webQrTrackId', trackId);
|
|
740
|
+
const loginAttrs = loginData.tokenAttrs && loginData.tokenAttrs.LOGIN;
|
|
741
|
+
const wtoken = loginAttrs && loginAttrs.token;
|
|
742
|
+
if (wtoken) {
|
|
743
|
+
this.session.set('webToken', wtoken);
|
|
744
|
+
}
|
|
745
|
+
if (loginData.deviceId != null) {
|
|
746
|
+
this.session.set('webDeviceId', loginData.deviceId);
|
|
747
|
+
}
|
|
748
|
+
if (loginData.clientSessionId != null) {
|
|
749
|
+
this.session.set('webClientSessionId', loginData.clientSessionId);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
console.log('✅ Вход в веб-версию по QR подтверждён (LOGIN_BY_QR, новое TCP-соединение)');
|
|
753
|
+
return loginData;
|
|
754
|
+
} catch (e) {
|
|
755
|
+
const msg = e && e.message ? e.message : String(e);
|
|
756
|
+
console.log(`LOGIN_BY_QR по новому TCP: ${msg} — пробуем WebSocket (Origin web.max.ru)…`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return await this._approveWebLoginByQrViaEphemeralWeb(trackId, options, qrUrlOrTrackId);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!this.isConnected) {
|
|
763
|
+
throw new Error('Нет соединения: сначала await client.connect()');
|
|
764
|
+
}
|
|
765
|
+
if (!this.isAuthorized) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
'Нужна авторизованная сессия: выполните start() или connect() + sync() с действующим токеном'
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
let lastErr;
|
|
772
|
+
const attempts = retry ? 2 : 1;
|
|
773
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
774
|
+
try {
|
|
775
|
+
const loginData = await this.loginByQR(trackId);
|
|
776
|
+
|
|
777
|
+
if (saveWebSession && loginData && typeof loginData === 'object') {
|
|
778
|
+
this.session.set('webQrTrackId', trackId);
|
|
779
|
+
const loginAttrs = loginData.tokenAttrs && loginData.tokenAttrs.LOGIN;
|
|
780
|
+
const wtoken = loginAttrs && loginAttrs.token;
|
|
781
|
+
if (wtoken) {
|
|
782
|
+
this.session.set('webToken', wtoken);
|
|
783
|
+
}
|
|
784
|
+
if (loginData.deviceId != null) {
|
|
785
|
+
this.session.set('webDeviceId', loginData.deviceId);
|
|
786
|
+
}
|
|
787
|
+
if (loginData.clientSessionId != null) {
|
|
788
|
+
this.session.set('webClientSessionId', loginData.clientSessionId);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
console.log('✅ Вход в веб-версию по QR подтверждён (LOGIN_BY_QR)');
|
|
793
|
+
return loginData;
|
|
794
|
+
} catch (e) {
|
|
795
|
+
lastErr = e;
|
|
796
|
+
if (attempt < attempts) {
|
|
797
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
throw e;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
throw lastErr;
|
|
804
|
+
}
|
|
805
|
+
|
|
349
806
|
/**
|
|
350
807
|
* Опрос статуса QR-кода
|
|
351
808
|
*/
|
|
@@ -406,7 +863,7 @@ class WebMaxClient extends EventEmitter {
|
|
|
406
863
|
}
|
|
407
864
|
|
|
408
865
|
console.log('Запрос QR-кода для привязки устройства...');
|
|
409
|
-
const response = await this.sendAndWait(Opcode.GET_QR,
|
|
866
|
+
const response = await this.sendAndWait(Opcode.GET_QR, undefined);
|
|
410
867
|
|
|
411
868
|
throwIfGetQRRejected(response.payload);
|
|
412
869
|
|
|
@@ -468,7 +925,7 @@ class WebMaxClient extends EventEmitter {
|
|
|
468
925
|
);
|
|
469
926
|
await webQr.connect();
|
|
470
927
|
|
|
471
|
-
const response = await webQr.sendAndWait(Opcode.GET_QR,
|
|
928
|
+
const response = await webQr.sendAndWait(Opcode.GET_QR, undefined);
|
|
472
929
|
throwIfGetQRRejected(response.payload);
|
|
473
930
|
|
|
474
931
|
const qrData = response.payload;
|
|
@@ -837,9 +1294,12 @@ class WebMaxClient extends EventEmitter {
|
|
|
837
1294
|
|
|
838
1295
|
return new Promise((resolve, reject) => {
|
|
839
1296
|
const headers = {
|
|
840
|
-
|
|
1297
|
+
Origin: this.origin,
|
|
841
1298
|
'User-Agent': this._handshakeUserAgent.headerUserAgent
|
|
842
1299
|
};
|
|
1300
|
+
if (this._wsReferer) {
|
|
1301
|
+
headers.Referer = this._wsReferer;
|
|
1302
|
+
}
|
|
843
1303
|
|
|
844
1304
|
this.ws = new WebSocket(this.apiUrl, {
|
|
845
1305
|
headers: headers
|
|
@@ -869,8 +1329,9 @@ class WebMaxClient extends EventEmitter {
|
|
|
869
1329
|
reject(error);
|
|
870
1330
|
});
|
|
871
1331
|
|
|
872
|
-
this.ws.on('close', () => {
|
|
873
|
-
|
|
1332
|
+
this.ws.on('close', (code, reason) => {
|
|
1333
|
+
const rs = reason && reason.length ? reason.toString() : '';
|
|
1334
|
+
console.log(`WebSocket соединение закрыто (code=${code}${rs ? `, ${rs}` : ''})`);
|
|
874
1335
|
this.isConnected = false;
|
|
875
1336
|
const err = new Error('Соединение закрыто');
|
|
876
1337
|
for (const [, pending] of this.pendingRequests) {
|
|
@@ -939,6 +1400,10 @@ class WebMaxClient extends EventEmitter {
|
|
|
939
1400
|
}
|
|
940
1401
|
break;
|
|
941
1402
|
|
|
1403
|
+
case Opcode.NOTIF_ATTACH:
|
|
1404
|
+
this._handleNotifAttach(data.payload);
|
|
1405
|
+
break;
|
|
1406
|
+
|
|
942
1407
|
default:
|
|
943
1408
|
this.emit('raw_message', data);
|
|
944
1409
|
}
|
|
@@ -952,10 +1417,13 @@ class WebMaxClient extends EventEmitter {
|
|
|
952
1417
|
* Обработка переподключения
|
|
953
1418
|
*/
|
|
954
1419
|
handleReconnect() {
|
|
1420
|
+
if (this.maxReconnectAttempts <= 0) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
955
1423
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
956
1424
|
this.reconnectAttempts++;
|
|
957
1425
|
console.log(`Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
|
|
958
|
-
|
|
1426
|
+
|
|
959
1427
|
setTimeout(() => {
|
|
960
1428
|
this.connect();
|
|
961
1429
|
}, this.reconnectDelay);
|
|
@@ -976,8 +1444,12 @@ class WebMaxClient extends EventEmitter {
|
|
|
976
1444
|
console.log(`📥 ${getOpcodeName(message.opcode)} (seq=${message.seq})${payload}`);
|
|
977
1445
|
}
|
|
978
1446
|
|
|
979
|
-
// Обработка ответов на запросы по seq
|
|
980
|
-
if (
|
|
1447
|
+
// Обработка ответов на запросы по seq (seq может быть 0 — не использовать truthiness)
|
|
1448
|
+
if (
|
|
1449
|
+
message.seq !== undefined &&
|
|
1450
|
+
message.seq !== null &&
|
|
1451
|
+
this.pendingRequests.has(message.seq)
|
|
1452
|
+
) {
|
|
981
1453
|
const pending = this.pendingRequests.get(message.seq);
|
|
982
1454
|
this.pendingRequests.delete(message.seq);
|
|
983
1455
|
|
|
@@ -1006,7 +1478,11 @@ class WebMaxClient extends EventEmitter {
|
|
|
1006
1478
|
case Opcode.PING:
|
|
1007
1479
|
await this.sendPong();
|
|
1008
1480
|
break;
|
|
1009
|
-
|
|
1481
|
+
|
|
1482
|
+
case Opcode.NOTIF_ATTACH:
|
|
1483
|
+
this._handleNotifAttach(message.payload);
|
|
1484
|
+
break;
|
|
1485
|
+
|
|
1010
1486
|
default:
|
|
1011
1487
|
this.emit('raw_message', message);
|
|
1012
1488
|
}
|
|
@@ -1079,17 +1555,21 @@ class WebMaxClient extends EventEmitter {
|
|
|
1079
1555
|
|
|
1080
1556
|
/**
|
|
1081
1557
|
* Создает сообщение в протоколе Max API
|
|
1558
|
+
* payload не включаем в JSON, если undefined — как web.max.ru для GET_QR (только opcode).
|
|
1082
1559
|
*/
|
|
1083
1560
|
makeMessage(opcode, payload, cmd = 0) {
|
|
1084
1561
|
this.seq += 1;
|
|
1085
|
-
|
|
1086
|
-
|
|
1562
|
+
|
|
1563
|
+
const message = {
|
|
1087
1564
|
ver: this.ver,
|
|
1088
1565
|
cmd: cmd,
|
|
1089
1566
|
seq: this.seq,
|
|
1090
1567
|
opcode: opcode,
|
|
1091
|
-
payload: payload
|
|
1092
1568
|
};
|
|
1569
|
+
if (payload !== undefined) {
|
|
1570
|
+
message.payload = payload;
|
|
1571
|
+
}
|
|
1572
|
+
return message;
|
|
1093
1573
|
}
|
|
1094
1574
|
|
|
1095
1575
|
/**
|
|
@@ -1192,6 +1672,73 @@ class WebMaxClient extends EventEmitter {
|
|
|
1192
1672
|
return Number.isNaN(n) ? chatId : n;
|
|
1193
1673
|
}
|
|
1194
1674
|
|
|
1675
|
+
/**
|
|
1676
|
+
* NOTIF_ATTACH (136): готовность вложения после загрузки видео/файла.
|
|
1677
|
+
*/
|
|
1678
|
+
_handleNotifAttach(payload) {
|
|
1679
|
+
if (!payload || typeof payload !== 'object') return;
|
|
1680
|
+
const vid = payload.videoId;
|
|
1681
|
+
if (vid != null) {
|
|
1682
|
+
const k = String(vid);
|
|
1683
|
+
const fn = this._uploadPendingVideo.get(k);
|
|
1684
|
+
if (fn) {
|
|
1685
|
+
this._uploadPendingVideo.delete(k);
|
|
1686
|
+
fn();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const fid = payload.fileId;
|
|
1690
|
+
if (fid != null) {
|
|
1691
|
+
const k = String(fid);
|
|
1692
|
+
const fn = this._uploadPendingFile.get(k);
|
|
1693
|
+
if (fn) {
|
|
1694
|
+
this._uploadPendingFile.delete(k);
|
|
1695
|
+
fn();
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* @param {Map<string, function(): void>} map
|
|
1702
|
+
*/
|
|
1703
|
+
_waitUploadNotif(map, id, label, timeoutMs = 120000) {
|
|
1704
|
+
return new Promise((resolve, reject) => {
|
|
1705
|
+
const k = String(id);
|
|
1706
|
+
const t = setTimeout(() => {
|
|
1707
|
+
map.delete(k);
|
|
1708
|
+
reject(new Error(`Таймаут ожидания NOTIF_ATTACH (${label})`));
|
|
1709
|
+
}, timeoutMs);
|
|
1710
|
+
map.set(k, () => {
|
|
1711
|
+
clearTimeout(t);
|
|
1712
|
+
resolve();
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async _postMultipartUpload(uploadUrl, buf, fname, mime) {
|
|
1718
|
+
const { Blob } = require('buffer');
|
|
1719
|
+
if (typeof fetch !== 'function') {
|
|
1720
|
+
throw new Error('upload: нужен Node.js 18+ с глобальным fetch');
|
|
1721
|
+
}
|
|
1722
|
+
const form = new FormData();
|
|
1723
|
+
form.append('file', new Blob([buf], { type: mime }), fname);
|
|
1724
|
+
const res = await fetch(uploadUrl, {
|
|
1725
|
+
method: 'POST',
|
|
1726
|
+
body: form,
|
|
1727
|
+
headers: {
|
|
1728
|
+
Accept: '*/*',
|
|
1729
|
+
'Accept-Language': 'ru-RU,ru;q=0.9',
|
|
1730
|
+
Origin: 'https://web.max.ru',
|
|
1731
|
+
Referer: 'https://web.max.ru/',
|
|
1732
|
+
'User-Agent': this.userAgent.headerUserAgent || 'Mozilla/5.0'
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
if (!res.ok) {
|
|
1736
|
+
const t = await res.text();
|
|
1737
|
+
throw new Error(`HTTP загрузка: ${res.status} ${t.slice(0, 300)}`);
|
|
1738
|
+
}
|
|
1739
|
+
return res;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1195
1742
|
/**
|
|
1196
1743
|
* Отправка сообщения (с уведомлением)
|
|
1197
1744
|
*/
|
|
@@ -1242,18 +1789,218 @@ class WebMaxClient extends EventEmitter {
|
|
|
1242
1789
|
return response.payload;
|
|
1243
1790
|
}
|
|
1244
1791
|
|
|
1792
|
+
/**
|
|
1793
|
+
* Загрузка локального изображения на сервер Max; результат передать в `attachments` у sendMessage / reply.
|
|
1794
|
+
* Схема: PHOTO_UPLOAD → UPLOAD_ATTACH_PREP → HTTP POST на выданный URL. Нужен Node 18+ (fetch, FormData).
|
|
1795
|
+
*
|
|
1796
|
+
* @param {number|string|bigint} chatId
|
|
1797
|
+
* @param {string} filePath путь к файлу (.png, .jpg, …)
|
|
1798
|
+
* @returns {Promise<{ _type: 'PHOTO', photoToken: string }>}
|
|
1799
|
+
*/
|
|
1800
|
+
async uploadPhoto(chatId, filePath) {
|
|
1801
|
+
const fsp = require('fs/promises');
|
|
1802
|
+
const path = require('path');
|
|
1803
|
+
|
|
1804
|
+
const buf = await fsp.readFile(filePath);
|
|
1805
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1806
|
+
const mime =
|
|
1807
|
+
ext === '.png'
|
|
1808
|
+
? 'image/png'
|
|
1809
|
+
: ext === '.webp'
|
|
1810
|
+
? 'image/webp'
|
|
1811
|
+
: ext === '.gif'
|
|
1812
|
+
? 'image/gif'
|
|
1813
|
+
: 'image/jpeg';
|
|
1814
|
+
const fname = path.basename(filePath) || 'image.jpg';
|
|
1815
|
+
|
|
1816
|
+
const r1 = await this.sendAndWait(Opcode.PHOTO_UPLOAD, { count: 1 });
|
|
1817
|
+
const p1 = r1.payload;
|
|
1818
|
+
if (p1 && p1.error) {
|
|
1819
|
+
const e = new Error(
|
|
1820
|
+
typeof p1.error === 'string' ? p1.error : JSON.stringify(p1.error)
|
|
1821
|
+
);
|
|
1822
|
+
e.rawPayload = p1;
|
|
1823
|
+
throw e;
|
|
1824
|
+
}
|
|
1825
|
+
const uploadUrl = p1 && p1.url;
|
|
1826
|
+
if (!uploadUrl) {
|
|
1827
|
+
throw new Error('PHOTO_UPLOAD: нет url в ответе');
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
await this.sendAndWait(Opcode.UPLOAD_ATTACH_PREP, {
|
|
1831
|
+
chatId: this._normalizeChatId(chatId),
|
|
1832
|
+
type: 'PHOTO'
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
const res = await this._postMultipartUpload(uploadUrl, buf, fname, mime);
|
|
1836
|
+
const obj = await res.json();
|
|
1837
|
+
const photos = obj.photos;
|
|
1838
|
+
let first;
|
|
1839
|
+
if (Array.isArray(photos)) {
|
|
1840
|
+
[first] = photos;
|
|
1841
|
+
} else if (photos && typeof photos === 'object') {
|
|
1842
|
+
first = Object.values(photos)[0];
|
|
1843
|
+
}
|
|
1844
|
+
const token = first && first.token;
|
|
1845
|
+
if (!token) {
|
|
1846
|
+
throw new Error(`PHOTO upload: неожиданный JSON: ${JSON.stringify(obj).slice(0, 400)}`);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
return {
|
|
1850
|
+
_type: 'PHOTO',
|
|
1851
|
+
photoToken: token
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* Загрузка видео; результат для `attachments: [{ _type: 'VIDEO', videoId, token }]`.
|
|
1857
|
+
* После HTTP POST ждёт NOTIF_ATTACH (opcode 136).
|
|
1858
|
+
*/
|
|
1859
|
+
async uploadVideo(chatId, filePath) {
|
|
1860
|
+
const fsp = require('fs/promises');
|
|
1861
|
+
const path = require('path');
|
|
1862
|
+
|
|
1863
|
+
const buf = await fsp.readFile(filePath);
|
|
1864
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1865
|
+
const fname = path.basename(filePath) || 'video.mp4';
|
|
1866
|
+
const mime =
|
|
1867
|
+
ext === '.webm' ? 'video/webm' : ext === '.mov' ? 'video/quicktime' : 'video/mp4';
|
|
1868
|
+
|
|
1869
|
+
const r = await this.sendAndWait(Opcode.VIDEO_UPLOAD, { count: 1 });
|
|
1870
|
+
const p = r.payload;
|
|
1871
|
+
if (p && p.error) {
|
|
1872
|
+
const e = new Error(
|
|
1873
|
+
typeof p.error === 'string' ? p.error : JSON.stringify(p.error)
|
|
1874
|
+
);
|
|
1875
|
+
e.rawPayload = p;
|
|
1876
|
+
throw e;
|
|
1877
|
+
}
|
|
1878
|
+
const info = p && p.info && p.info[0];
|
|
1879
|
+
if (!info) {
|
|
1880
|
+
throw new Error('VIDEO_UPLOAD: нет info в ответе');
|
|
1881
|
+
}
|
|
1882
|
+
const { url: uploadUrl, videoId, token } = info;
|
|
1883
|
+
if (!uploadUrl || videoId == null || token == null) {
|
|
1884
|
+
throw new Error('VIDEO_UPLOAD: нет url, videoId или token');
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
const waitReady = this._waitUploadNotif(this._uploadPendingVideo, videoId, 'VIDEO');
|
|
1888
|
+
|
|
1889
|
+
await this.sendAndWait(Opcode.UPLOAD_ATTACH_PREP, {
|
|
1890
|
+
chatId: this._normalizeChatId(chatId),
|
|
1891
|
+
type: 'VIDEO'
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
await this._postMultipartUpload(uploadUrl, buf, fname, mime);
|
|
1895
|
+
|
|
1896
|
+
await waitReady;
|
|
1897
|
+
|
|
1898
|
+
return { _type: 'VIDEO', videoId, token };
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Загрузка произвольного файла (документ, архив, **аудио** и т.д.) для `attachments: [{ _type: 'FILE', fileId }]`.
|
|
1903
|
+
* После HTTP POST ждёт NOTIF_ATTACH.
|
|
1904
|
+
*
|
|
1905
|
+
* @param {{ filename?: string, mimeType?: string }} [options]
|
|
1906
|
+
*/
|
|
1907
|
+
async uploadFile(chatId, filePath, options = {}) {
|
|
1908
|
+
const fsp = require('fs/promises');
|
|
1909
|
+
const path = require('path');
|
|
1910
|
+
|
|
1911
|
+
const buf = await fsp.readFile(filePath);
|
|
1912
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1913
|
+
const fname = options.filename || path.basename(filePath) || 'file.bin';
|
|
1914
|
+
const mime =
|
|
1915
|
+
options.mimeType ||
|
|
1916
|
+
this._mimeGuessForFile(ext);
|
|
1917
|
+
|
|
1918
|
+
const r = await this.sendAndWait(Opcode.FILE_UPLOAD, { count: 1 });
|
|
1919
|
+
const p = r.payload;
|
|
1920
|
+
if (p && p.error) {
|
|
1921
|
+
const e = new Error(
|
|
1922
|
+
typeof p.error === 'string' ? p.error : JSON.stringify(p.error)
|
|
1923
|
+
);
|
|
1924
|
+
e.rawPayload = p;
|
|
1925
|
+
throw e;
|
|
1926
|
+
}
|
|
1927
|
+
const info = p && p.info && p.info[0];
|
|
1928
|
+
if (!info) {
|
|
1929
|
+
throw new Error('FILE_UPLOAD: нет info в ответе');
|
|
1930
|
+
}
|
|
1931
|
+
const { url: uploadUrl, fileId } = info;
|
|
1932
|
+
if (!uploadUrl || fileId == null) {
|
|
1933
|
+
throw new Error('FILE_UPLOAD: нет url или fileId');
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const waitReady = this._waitUploadNotif(this._uploadPendingFile, fileId, 'FILE');
|
|
1937
|
+
|
|
1938
|
+
await this.sendAndWait(Opcode.UPLOAD_ATTACH_PREP, {
|
|
1939
|
+
chatId: this._normalizeChatId(chatId),
|
|
1940
|
+
type: 'FILE'
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
await this._postMultipartUpload(uploadUrl, buf, fname, mime);
|
|
1944
|
+
|
|
1945
|
+
await waitReady;
|
|
1946
|
+
|
|
1947
|
+
return { _type: 'FILE', fileId };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* Загрузка аудио как файла (удобно для .mp3, .ogg, .m4a, .wav).
|
|
1952
|
+
* Внутри вызывает uploadFile() с подходящим MIME.
|
|
1953
|
+
*/
|
|
1954
|
+
async uploadAudio(chatId, filePath) {
|
|
1955
|
+
const path = require('path');
|
|
1956
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1957
|
+
const mime =
|
|
1958
|
+
ext === '.mp3'
|
|
1959
|
+
? 'audio/mpeg'
|
|
1960
|
+
: ext === '.ogg' || ext === '.oga'
|
|
1961
|
+
? 'audio/ogg'
|
|
1962
|
+
: ext === '.m4a' || ext === '.aac'
|
|
1963
|
+
? 'audio/mp4'
|
|
1964
|
+
: ext === '.wav'
|
|
1965
|
+
? 'audio/wav'
|
|
1966
|
+
: ext === '.flac'
|
|
1967
|
+
? 'audio/flac'
|
|
1968
|
+
: 'audio/mpeg';
|
|
1969
|
+
return this.uploadFile(chatId, filePath, { mimeType: mime });
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
_mimeGuessForFile(ext) {
|
|
1973
|
+
const m = {
|
|
1974
|
+
'.png': 'image/png',
|
|
1975
|
+
'.jpg': 'image/jpeg',
|
|
1976
|
+
'.jpeg': 'image/jpeg',
|
|
1977
|
+
'.gif': 'image/gif',
|
|
1978
|
+
'.webp': 'image/webp',
|
|
1979
|
+
'.pdf': 'application/pdf',
|
|
1980
|
+
'.zip': 'application/zip',
|
|
1981
|
+
'.mp3': 'audio/mpeg',
|
|
1982
|
+
'.ogg': 'audio/ogg',
|
|
1983
|
+
'.m4a': 'audio/mp4',
|
|
1984
|
+
'.wav': 'audio/wav',
|
|
1985
|
+
'.flac': 'audio/flac',
|
|
1986
|
+
'.mp4': 'video/mp4',
|
|
1987
|
+
'.webm': 'video/webm'
|
|
1988
|
+
};
|
|
1989
|
+
return m[ext] || 'application/octet-stream';
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1245
1992
|
/**
|
|
1246
1993
|
* Редактирование сообщения
|
|
1247
1994
|
*/
|
|
1248
1995
|
async editMessage(options) {
|
|
1249
|
-
const { messageId, chatId, text } = options;
|
|
1996
|
+
const { messageId, chatId, text, attachments } = options;
|
|
1250
1997
|
|
|
1251
1998
|
const payload = {
|
|
1252
1999
|
chatId: chatId,
|
|
1253
2000
|
messageId: messageId,
|
|
1254
2001
|
text: text || '',
|
|
1255
2002
|
elements: [],
|
|
1256
|
-
attaches: []
|
|
2003
|
+
attaches: Array.isArray(attachments) && attachments.length ? attachments : []
|
|
1257
2004
|
};
|
|
1258
2005
|
|
|
1259
2006
|
const response = await this.sendAndWait(Opcode.MSG_EDIT, payload);
|
|
@@ -1354,6 +2101,360 @@ class WebMaxClient extends EventEmitter {
|
|
|
1354
2101
|
return messages.map(msg => new Message(msg, this));
|
|
1355
2102
|
}
|
|
1356
2103
|
|
|
2104
|
+
/**
|
|
2105
|
+
* Закрепить сообщение в чате (CHAT_UPDATE).
|
|
2106
|
+
*/
|
|
2107
|
+
async pinMessage({ chatId, messageId, notifyPin = false }) {
|
|
2108
|
+
return await this.sendAndWait(Opcode.CHAT_UPDATE, {
|
|
2109
|
+
chatId: this._normalizeChatId(chatId),
|
|
2110
|
+
messageId: String(messageId),
|
|
2111
|
+
notifyPin: !!notifyPin
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/**
|
|
2116
|
+
* Поставить реакцию-эмодзи на сообщение.
|
|
2117
|
+
*/
|
|
2118
|
+
async setMessageReaction({ chatId, messageId, emoji }) {
|
|
2119
|
+
return await this.sendAndWait(Opcode.MSG_REACTION, {
|
|
2120
|
+
chatId: this._normalizeChatId(chatId),
|
|
2121
|
+
messageId: String(messageId),
|
|
2122
|
+
reaction: {
|
|
2123
|
+
reactionType: 'EMOJI',
|
|
2124
|
+
id: String(emoji)
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* Снять свою реакцию с сообщения.
|
|
2131
|
+
*/
|
|
2132
|
+
async cancelMessageReaction({ chatId, messageId }) {
|
|
2133
|
+
return await this.sendAndWait(Opcode.MSG_CANCEL_REACTION, {
|
|
2134
|
+
chatId: this._normalizeChatId(chatId),
|
|
2135
|
+
messageId: String(messageId)
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Список реакций на сообщение.
|
|
2141
|
+
*/
|
|
2142
|
+
async getMessageReactions({ chatId, messageId, count = 100 }) {
|
|
2143
|
+
return await this.sendAndWait(Opcode.MSG_GET_REACTIONS, {
|
|
2144
|
+
chatId: this._normalizeChatId(chatId),
|
|
2145
|
+
messageId: String(messageId),
|
|
2146
|
+
count
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Информация о чатах по id (opcode 48).
|
|
2152
|
+
*/
|
|
2153
|
+
async getChatInfo(chatIds) {
|
|
2154
|
+
const ids = Array.isArray(chatIds) ? chatIds : [chatIds];
|
|
2155
|
+
const response = await this.sendAndWait(Opcode.CHAT_INFO, { chatIds: ids });
|
|
2156
|
+
return response.payload;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/**
|
|
2160
|
+
* Разрешить ссылку: канал, инвайт join/…, URL max.ru (LINK_INFO).
|
|
2161
|
+
*/
|
|
2162
|
+
async resolveLink(link) {
|
|
2163
|
+
const response = await this.sendAndWait(Opcode.LINK_INFO, { link: String(link) });
|
|
2164
|
+
return response.payload;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Вступить по ссылке (канал, группа и т.д.).
|
|
2169
|
+
*/
|
|
2170
|
+
async joinChatByLink(link) {
|
|
2171
|
+
const response = await this.sendAndWait(Opcode.CHAT_JOIN, { link: String(link) });
|
|
2172
|
+
return response.payload;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/**
|
|
2176
|
+
* Подписка / отписка на канал.
|
|
2177
|
+
*/
|
|
2178
|
+
async setChatSubscription(chatId, subscribe) {
|
|
2179
|
+
return await this.sendAndWait(Opcode.CHAT_SUBSCRIBE, {
|
|
2180
|
+
chatId: this._normalizeChatId(chatId),
|
|
2181
|
+
subscribe: !!subscribe
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/**
|
|
2186
|
+
* Создать групповой чат (CONTROL в MSG_SEND).
|
|
2187
|
+
*/
|
|
2188
|
+
async createGroup({ title, userIds }) {
|
|
2189
|
+
const cid = this._nextClientMessageId();
|
|
2190
|
+
return await this.sendAndWait(Opcode.MSG_SEND, {
|
|
2191
|
+
message: {
|
|
2192
|
+
text: '',
|
|
2193
|
+
cid,
|
|
2194
|
+
elements: [],
|
|
2195
|
+
attaches: [
|
|
2196
|
+
{
|
|
2197
|
+
_type: 'CONTROL',
|
|
2198
|
+
event: 'new',
|
|
2199
|
+
chatType: 'CHAT',
|
|
2200
|
+
title,
|
|
2201
|
+
userIds
|
|
2202
|
+
}
|
|
2203
|
+
]
|
|
2204
|
+
},
|
|
2205
|
+
notify: true
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Создать канал (CONTROL в MSG_SEND).
|
|
2211
|
+
*/
|
|
2212
|
+
async createChannel({ title }) {
|
|
2213
|
+
const cid = this._nextClientMessageId();
|
|
2214
|
+
return await this.sendAndWait(Opcode.MSG_SEND, {
|
|
2215
|
+
message: {
|
|
2216
|
+
text: '',
|
|
2217
|
+
cid,
|
|
2218
|
+
elements: [],
|
|
2219
|
+
attaches: [
|
|
2220
|
+
{
|
|
2221
|
+
_type: 'CONTROL',
|
|
2222
|
+
event: 'new',
|
|
2223
|
+
title,
|
|
2224
|
+
chatType: 'CHANNEL'
|
|
2225
|
+
}
|
|
2226
|
+
]
|
|
2227
|
+
},
|
|
2228
|
+
notify: true
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
/**
|
|
2233
|
+
* Отключить уведомления в чате (CONFIG), как «не беспокоить» для чата.
|
|
2234
|
+
*/
|
|
2235
|
+
async muteChat(chatId, mute = true) {
|
|
2236
|
+
const id = String(this._normalizeChatId(chatId));
|
|
2237
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
2238
|
+
settings: {
|
|
2239
|
+
chats: {
|
|
2240
|
+
[id]: {
|
|
2241
|
+
dontDisturbUntil: mute ? -1 : 0
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* Участники чата (не более 500 за запрос).
|
|
2250
|
+
*/
|
|
2251
|
+
async getChatMembers({ chatId, marker = 0, count = 500, type = 'MEMBER' }) {
|
|
2252
|
+
if (count > 500) {
|
|
2253
|
+
throw new Error('getChatMembers: count не больше 500');
|
|
2254
|
+
}
|
|
2255
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS, {
|
|
2256
|
+
type,
|
|
2257
|
+
marker,
|
|
2258
|
+
chatId: this._normalizeChatId(chatId),
|
|
2259
|
+
count
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Пригласить пользователей в чат.
|
|
2265
|
+
*/
|
|
2266
|
+
async inviteToChat({ chatId, userIds, showHistory = true }) {
|
|
2267
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
2268
|
+
chatId: this._normalizeChatId(chatId),
|
|
2269
|
+
userIds,
|
|
2270
|
+
showHistory,
|
|
2271
|
+
operation: 'add'
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/**
|
|
2276
|
+
* Исключить пользователей из чата.
|
|
2277
|
+
*/
|
|
2278
|
+
async removeFromChat({ chatId, userIds, cleanMsgPeriod = 0 }) {
|
|
2279
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
2280
|
+
chatId: this._normalizeChatId(chatId),
|
|
2281
|
+
userIds,
|
|
2282
|
+
operation: 'remove',
|
|
2283
|
+
cleanMsgPeriod
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Назначить администраторов. `permissions` — битовая маска прав (по умолчанию 120).
|
|
2289
|
+
*/
|
|
2290
|
+
async addChatAdmins({ chatId, userIds, permissions = 120 }) {
|
|
2291
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
2292
|
+
chatId: this._normalizeChatId(chatId),
|
|
2293
|
+
userIds,
|
|
2294
|
+
type: 'ADMIN',
|
|
2295
|
+
operation: 'add',
|
|
2296
|
+
permissions
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Снять права администратора.
|
|
2302
|
+
*/
|
|
2303
|
+
async removeChatAdmins({ chatId, userIds }) {
|
|
2304
|
+
return await this.sendAndWait(Opcode.CHAT_MEMBERS_UPDATE, {
|
|
2305
|
+
chatId: this._normalizeChatId(chatId),
|
|
2306
|
+
userIds,
|
|
2307
|
+
type: 'ADMIN',
|
|
2308
|
+
operation: 'remove'
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/**
|
|
2313
|
+
* Передать владение группой.
|
|
2314
|
+
*/
|
|
2315
|
+
async transferChatOwnership({ chatId, newOwnerId }) {
|
|
2316
|
+
return await this.sendAndWait(Opcode.CHAT_UPDATE, {
|
|
2317
|
+
chatId: this._normalizeChatId(chatId),
|
|
2318
|
+
changeOwnerId: newOwnerId
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Настройки группы: например ONLY_OWNER_CAN_CHANGE_ICON_TITLE, ALL_CAN_PIN_MESSAGE, ONLY_ADMIN_CAN_ADD_MEMBER.
|
|
2324
|
+
*/
|
|
2325
|
+
async setGroupOptions({ chatId, options }) {
|
|
2326
|
+
return await this.sendAndWait(Opcode.CHAT_UPDATE, {
|
|
2327
|
+
chatId: this._normalizeChatId(chatId),
|
|
2328
|
+
options
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Несколько контактов по id (сырой ответ CONTACT_INFO).
|
|
2334
|
+
*/
|
|
2335
|
+
async getContacts(contactIds) {
|
|
2336
|
+
const ids = Array.isArray(contactIds) ? contactIds : [contactIds];
|
|
2337
|
+
const response = await this.sendAndWait(Opcode.CONTACT_INFO, { contactIds: ids });
|
|
2338
|
+
return response.payload;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Добавить пользователя в контакты.
|
|
2343
|
+
*/
|
|
2344
|
+
async addContact(userId) {
|
|
2345
|
+
return await this.sendAndWait(Opcode.CONTACT_UPDATE, {
|
|
2346
|
+
contactId: userId,
|
|
2347
|
+
action: 'ADD'
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/**
|
|
2352
|
+
* Заблокировать пользователя.
|
|
2353
|
+
*/
|
|
2354
|
+
async blockUser(userId) {
|
|
2355
|
+
return await this.sendAndWait(Opcode.CONTACT_UPDATE, {
|
|
2356
|
+
contactId: userId,
|
|
2357
|
+
action: 'BLOCK'
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* Изменить своё имя / описание (PROFILE).
|
|
2363
|
+
*/
|
|
2364
|
+
async updateProfile({ firstName, lastName, description } = {}) {
|
|
2365
|
+
const payload = {};
|
|
2366
|
+
if (firstName !== undefined) payload.firstName = firstName;
|
|
2367
|
+
if (lastName !== undefined) payload.lastName = lastName;
|
|
2368
|
+
if (description !== undefined) payload.description = description;
|
|
2369
|
+
return await this.sendAndWait(Opcode.PROFILE, payload);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Скрыть статус «в сети» для других.
|
|
2374
|
+
*/
|
|
2375
|
+
async setHiddenOnline(hidden) {
|
|
2376
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
2377
|
+
settings: {
|
|
2378
|
+
user: { HIDDEN: !!hidden }
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Кто может найти вас по телефону: 'ALL' | 'CONTACTS' или true/false (как ALL/CONTACTS).
|
|
2385
|
+
*/
|
|
2386
|
+
async setFindableByPhone(mode) {
|
|
2387
|
+
const v =
|
|
2388
|
+
mode === true || mode === 'ALL'
|
|
2389
|
+
? 'ALL'
|
|
2390
|
+
: mode === false || mode === 'CONTACTS'
|
|
2391
|
+
? 'CONTACTS'
|
|
2392
|
+
: String(mode);
|
|
2393
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
2394
|
+
settings: {
|
|
2395
|
+
user: { SEARCH_BY_PHONE: v }
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
/**
|
|
2401
|
+
* Кто может звонить: 'ALL' | 'CONTACTS'.
|
|
2402
|
+
*/
|
|
2403
|
+
async setCallsPrivacyMode(mode) {
|
|
2404
|
+
const v =
|
|
2405
|
+
mode === true || mode === 'ALL'
|
|
2406
|
+
? 'ALL'
|
|
2407
|
+
: mode === false || mode === 'CONTACTS'
|
|
2408
|
+
? 'CONTACTS'
|
|
2409
|
+
: String(mode);
|
|
2410
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
2411
|
+
settings: {
|
|
2412
|
+
user: { INCOMING_CALL: v }
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* Кто может приглашать вас в чаты: 'ALL' | 'CONTACTS'.
|
|
2419
|
+
*/
|
|
2420
|
+
async setChatsInvitePrivacy(mode) {
|
|
2421
|
+
const v =
|
|
2422
|
+
mode === true || mode === 'ALL'
|
|
2423
|
+
? 'ALL'
|
|
2424
|
+
: mode === false || mode === 'CONTACTS'
|
|
2425
|
+
? 'CONTACTS'
|
|
2426
|
+
: String(mode);
|
|
2427
|
+
return await this.sendAndWait(Opcode.CONFIG, {
|
|
2428
|
+
settings: {
|
|
2429
|
+
user: { CHATS_INVITE: v }
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
/**
|
|
2435
|
+
* Удобно: канал по @username (resolveLink на https://max.ru/username).
|
|
2436
|
+
*/
|
|
2437
|
+
async resolveChannelByUsername(username) {
|
|
2438
|
+
const u = String(username).replace(/^@/, '');
|
|
2439
|
+
return this.resolveLink(`https://max.ru/${u}`);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Вступить в канал по @username.
|
|
2444
|
+
*/
|
|
2445
|
+
async joinChannelByUsername(username) {
|
|
2446
|
+
const u = String(username).replace(/^@/, '');
|
|
2447
|
+
return this.joinChatByLink(`https://max.ru/${u}`);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/**
|
|
2451
|
+
* Инвайт по хэшу из ссылки join/XXXX.
|
|
2452
|
+
*/
|
|
2453
|
+
async resolveInviteHash(hash) {
|
|
2454
|
+
const h = String(hash).replace(/^join\//, '');
|
|
2455
|
+
return this.resolveLink(`join/${h}`);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
1357
2458
|
/**
|
|
1358
2459
|
* Выполнение зарегистрированных обработчиков
|
|
1359
2460
|
*/
|